首页/新闻资讯/正文详情

Sourcery AutoEquatable 模板实战:自动生成 Swift Equatable 实现,彻底告别样板代码

发布时间:2026/9/26 3:02:12 来源:云帆数科 栏目:资讯中心
Sourcery AutoEquatable 模板实战:自动生成 Swift Equatable 实现,彻底告别样板代码
代码生成开发工具【免费下载链接】SourceryMeta-programming for Swift, stop writing boilerplate code.项目地址https://gitcode.com/gh_mirrors/so/Sourcery点击查看免费下载Sourcery 是面向 Swift 的元编程Meta-programming工具通过扫描源码并渲染模板来批量生成代码。本文以 guides/Equatable.md 为核心深入讲解其内置AutoEquatable模板如何让任意类型类、结构体、枚举、协议自动获得Equatable一致性以及skipEquality、arrayEquality两个变量级注解的用法并结合模板源码与测试用例揭示生成逻辑的底层细节。读完本文你将能够在自己的项目中直接配置并运行 Sourcery一键生成可编译、可维护的实现。一、AutoEquatable 是什么在 Swift 中为自定义类型实现Equatable通常意味着手写大量重复的函数逐一比较每个存储属性处理可选值、数组、枚举关联值……而 Sourcery 的AutoEquatable模板仓库路径 Templates/Templates/AutoEquatable.stencil可以把这件工作交给机器完成。根据 guides/Equatable.md 的定义该模板用于为满足以下任一条件的类型生成相等性比较代码显式遵循名为AutoEquatable的协议在源码注释中标注了AutoEquatable注解即 guides/Writing templates.md 中介绍的 source annotations 机制。AutoEquatable本身是一个幽灵协议Phantom Protocol只在仓库的源码中充当标记并不携带任何要求。它定义在 SourceryRuntime/Sources/Common/AST/PhantomProtocols.swift 中/// Phantom protocol for equality protocol AutoEquatable {}与它并列的还有AutoDiffable、AutoDescription、AutoCoding、AutoJSExport等它们共同构成 Sourcery 模板体系的触发标记。二、两种触发方式2.1 协议标记conformance在类型声明中显式遵循AutoEquatablestruct User: AutoEquatable { let name: String let age: Int }2.2 注解标记annotation在类型声明上方的注释块中写入// sourcery: AutoEquatable// sourcery: AutoEquatable class Profile { var avatarURL: URL? }注解方式的好处是不必让业务类型与 Sourcery 的协议产生耦合。根据 guides/Writing templates.md注解可以写在声明上方也可以写在行尾、或以sourcery:begin/sourcery:end区间作用于多个声明还能通过sourcery:file:作用于整个文件。三、不同类型的不同生成策略模板对不同类型的处理是有意区分的理解这一点是正确使用的前提类型是否添加: Equatable一致性生成内容结构体 / 普通类是extension {{ type.name }}: Equatable {}函数逐属性比较存储属性协议否避免变成 PAT即 Protocol with Associated Types仅生成func 继承自 NSObject 的类否NSObject 已有isEqual:语义仅生成func 枚举是基于switch (lhs, rhs)逐 case 比较含关联值展开有父类/超类的类型是但会插入警告注释不继承父类的比较逻辑见下文局限从 Templates/Templates/AutoEquatable.stencil 的源码可以看到对类、结构体、协议的生成循环是这样的{% for type in types.types|!enum where type.implements.AutoEquatable or type|annotated:AutoEquatable %} // MARK: - {{ type.name }} AutoEquatable {% if not type.kind protocol and not type.based.NSObject %}extension {{ type.name }}: Equatable {}{% endif %} {{ type.accessLevel }} func (lhs:{% if type.kind protocol %} any{% endif %} {{ type.name }}, rhs:{% if type.kind protocol %} any{% endif %} {{ type.name }}) - Bool { ... } {% endfor %}其中几个关键细节过滤条件types.types|!enum表示排除枚举枚举走单独的循环type.implements.AutoEquatable对应协议方式type|annotated:AutoEquatable对应注解方式函数的访问级别取自type.accessLevel即类型自身的访问级别这一点在 CHANGELOG.md 中有明确记录AutoEquatable will use type.accessLevel for its function, closes #675对协议生成比较函数时参数类型会加上any前缀即func (lhs: any SomeProtocol, rhs: any SomeProtocol)这是为了适配 Swift 5.7 的 existentials 写法CHANGELOG 中记录了该修复fix: AutoEquatable Stencil to useanyfor protocols。3.1 为什么协议不加 Equatable 一致性原文档给出的理由很明确如果给协议加上: Equatable协议就会变成 PATProtocol with Associated Types导致它无法作为普通类型使用例如放进数组、作为属性类型都会受Self约束限制。因此模板对协议只生成函数把一致性交给具体遵循者。四、变量级注解skipEquality 与 arrayEquality这是 guides/Equatable.md 重点介绍的可用变量注解它们作用于具体属性变量而非类型。4.1 skipEquality跳过某个属性的比较当某个属性不应参与相等性判断时在其声明上方标注// sourcery: skipEqualitystruct CachedItem: AutoEquatable { let id: String // sourcery: skipEquality var cacheTimestamp: Date // 不参与比较 }模板中对应过滤逻辑位于 Templates/Templates/AutoEquatable.stencil{% for variable in variables where variable.readAccess ! private and variable.readAccess ! fileprivate %}{% if not variable.annotations.skipEquality %}guard ... {% endif %}注意这里同时过滤掉了private和fileprivate访问级别的属性——私有存储属性不会出现在生成的中这也是 CHANGELOG 中Fixed regression in AutoEquatable AutoHashable template with private computed variables等修复累积下来的行为。4.2 arrayEquality数组元素的逐项比较当属性是数组、且元素类型本身没有遵循Equatable但提供了运算符典型场景是协议类型的数组时直接写lhs.array rhs.array会编译失败。此时标注// sourcery: arrayEquality模板会改用compareArrays辅助函数逐项比较struct Menu: AutoEquatable { // sourcery: arrayEquality let items: [MenuItem] // MenuItem 是协议但实现了 }模板分支AutoEquatable.stencil{% if not variable.annotations.arrayEquality %}lhs.{{ variable.name }} rhs.{{ variable.name }}{% else %}compareArrays(lhs: lhs.{{ variable.name }}, rhs: rhs.{{ variable.name }}, compare: ){% endif %}生成的compareArrays会先比较数组长度再逐个元素调用传入的比较闭包fileprivate func compareArraysT(lhs: [T], rhs: [T], compare: (_ lhs: T, _ rhs: T) - Bool) - Bool { guard lhs.count rhs.count else { return false } for (idx, lhsItem) in lhs.enumerated() { guard compare(lhsItem, rhs[idx]) else { return false } } return true }4.3 可选值自动使用 compareOptionals无需任何注解模板对可选属性包括隐式解包可选Int!会自动调用compareOptionalsfileprivate func compareOptionalsT(lhs: T?, rhs: T?, compare: (_ lhs: T, _ rhs: T) - Bool) - Bool { switch (lhs, rhs) { case let (lValue?, rValue?): return compare(lValue, rValue) case (nil, nil): return true default: return false } }该函数正确处理了两者都有值、两者都为空、一方为空三种情况避免手写时容易遗漏的边界。五、完整示例输出guides/Equatable.md 给出了如下示例输出结构体场景// MARK: - AdNodeViewModel AutoEquatable extension AdNodeViewModel: Equatable {} internal func (lhs: AdNodeViewModel, rhs: AdNodeViewModel) - Bool { guard lhs.remoteAdView rhs.remoteAdView else { return false } guard lhs.hidesDisclaimer rhs.hidesDisclaimer else { return false } guard lhs.type rhs.type else { return false } guard lhs.height rhs.height else { return false } guard lhs.attributedDisclaimer rhs.attributedDisclaimer else { return false } return true }生成逻辑是一系列guard ... else { return false }短路比较任何属性不等立即返回false全部相等才返回true。这种写法比累加等值数量的方式更清晰也能与编译器的性能优化良好配合。六、枚举的相等性生成枚举走独立的生成循环AutoEquatable.stencil基于switch (lhs, rhs)展开{% for type in types.enums where type.implements.AutoEquatable or type|annotated:AutoEquatable %} extension {{ type.name }}: Equatable {} {{ type.accessLevel }} func (lhs: {{ type.name }}, rhs: {{ type.name }}) - Bool { switch (lhs, rhs) { ... {{ default: return false if type.cases.count 1 }} } } {% endfor %}三类 case 的处理各不相同无关联值 case直接case (.one, .one): return true单个关联值 case模式绑定后直接return lhs rhs多个关联值 case逐一比较每个关联值if lhsFirst ! rhsFirst { return false }全部通过才return true。一个值得注意的细节只有 case 数量大于 1 时才生成default: return false。因为当枚举只有一个 case 时switch已经穷尽了所有可能再加default反而产生unreachable code编译警告。这一行为在 Templates/Tests/Context/AutoEquatable.swift 的测试输入单 case 枚举AutoEquatableEnumWithOneCase与对应期望输出中得到了验证。七、模板源码逐段解析整体来看AutoEquatable.stencil 由三部分构成两个 fileprivate 辅助函数compareOptionals与compareArrays前文已述它们会被原样注入每个生成文件类 / 结构体 / 协议循环types.types|!enum过滤枚举后遍历compareVariables宏对storedVariables非协议类型或allVariables协议类型展开比较注意协议比较的是allVariables含计算属性因为协议的属性都是计算性质的枚举循环types.enums单独遍历。compareVariables宏的核心逻辑AutoEquatable.stencil可概括为对每个 非private/fileprivate 且 未标注 skipEquality 的变量 guard 比较语句 else { return false } 其中 可选值 → compareOptionals(lhs:..., rhs:..., compare: ) arrayEquality → compareArrays(lhs:..., rhs:..., compare: ) 普通值 → lhs.属性 rhs.属性八、测试如何验证这些行为仓库用生成-对比的方式测试该模板测试装置位于 Templates/Tests/TemplatesTests.swift它实际调用 Sourcery 可执行文件处理测试输入再把生成结果与期望文件逐行对比忽略空白与注释差异。测试输入 Templates/Tests/Context/AutoEquatable.swift 覆盖了以下边界场景含private/fileprivate属性的结构体与类——期望输出中这两类属性被跳过标注arrayEquality的协议数组属性——期望输出调用compareArrays可选属性与隐式解包可选属性Int!——期望输出调用compareOptionals单 case 枚举——期望输出没有default分支多关联值枚举——期望输出逐值比较继承场景——期望输出包含警告注释NSObject 子类——期望输出不添加Equatable一致性注解触发的类——验证注解方式可用。对照期望输出 Templates/Tests/Expected/AutoEquatable.expected可以看到生成结果的几个关键形态// 类跳过 private/fileprivate数组与可选走辅助函数 internal func (lhs: AutoEquatableClass, rhs: AutoEquatableClass) - Bool { guard lhs.firstName rhs.firstName else { return false } guard lhs.lastName rhs.lastName else { return false } guard compareArrays(lhs: lhs.parents, rhs: rhs.parents, compare: ) else { return false } guard compareOptionals(lhs: lhs.age, rhs: rhs.age, compare: ) else { return false } guard lhs.moneyInThePocket rhs.moneyInThePocket else { return false } guard compareOptionals(lhs: lhs.friends, rhs: rhs.friends, compare: ) else { return false } return true } // 协议不添加 Equatable 一致性参数使用 any internal func (lhs: any AutoEquatableProtocol, rhs: any AutoEquatableProtocol) - Bool { guard lhs.width rhs.width else { return false } guard lhs.height rhs.height else { return false } guard lhs.name rhs.name else { return false } return true } // 多关联值枚举逐值比较 case let (.two(lhsFirst, lhsSecond), .two(rhsFirst, rhsSecond)): if lhsFirst ! rhsFirst { return false } if lhsSecond ! rhsSecond { return false } return true九、已知局限继承与 NSObject模板对继承的处理是显式的不支持这一点必须在使用前了解清楚。9.1 不支持继承如果被标记的类型存在父类且父类也参与 AutoEquatable 生成模板会输出一行注释THIS WONT COMPILE, WE DONT SUPPORT INHERITANCE for AutoEquatable对应的模板代码是 AutoEquatable.stencil{% if type.supertype.based.Equatable or type.supertype.implements.AutoEquatable or type.supertype|annotated:AutoEquatable %}THIS WONT COMPILE, WE DONT SUPPORT INHERITANCE for AutoEquatable{% endif %}注意这行文本是模板刻意生成的编译失败提示提醒开发者子类的只比较了子类自己的存储属性没有也无法安全地比较父类的属性强行使用会导致等值判断不完整。测试输入 Templates/Tests/Context/AutoEquatable.swift 中的AutoEquatableClassInherited和 Templates/Tests/Expected/AutoEquatable.expected 的期望输出都明确体现了这一行为。9.2 NSObject 子类不加 Equatable 一致性当类继承自NSObject时模板不会添加extension X: Equatable {}因为NSObject已经通过isEqual:提供了一套相等性语义但仍会生成函数。测试输入中的AutoEquatableNSObjectTemplates/Tests/Context/AutoEquatable.swift正是为此设计的。十、如何在项目中运行10.1 命令行方式根据 guides/Usage.mdSourcery 是命令行工具基本用法为$ ./sourcery --sources sources path --templates templates path --output output path针对 AutoEquatable把模板路径指向仓库内的Templates/Templates/AutoEquatable.stencil或你的模板目录即可。常用选项包括--sources要扫描的 Swift 源码路径可多次指定--templates模板文件或目录可多次指定--output输出路径默认当前目录指向目录时每个模板生成一个TemplateName.generated.swift--watch监听源码与模板变化并自动重新生成配合 Sourcery 内置 daemon 可以边改模板边看生成结果--disableCache关闭解析缓存调试模板时使用--prune删除生成的空文件--verbose/--quiet控制日志级别。10.2 配置文件方式更推荐的做法是把配置写入.sourcery.ymlSourcery 会在当前路径或--config指定路径下查找sources: - sources path templates: - templates path output: output path其中include/exclude键可以对源码与模板做更细粒度的包含排除这是命令行没有的能力。注意配置文件中的路径默认相对于配置文件所在目录。10.3 需要遵循的契约在源码侧只需保证两点让目标类型遵循AutoEquatable或加注解在模板渲染范围内包含 AutoEquatable.stencil。仓库内置的测试就是标准姿势的完整样例TemplatesTests.generateFiles()Templates/Tests/TemplatesTests.swift以Context目录为源码、Templates目录为模板、Generated为输出运行 Sourcery再与Expected对比。你可以按同样方式组织自己的工程。十一、备选方案针对 NSObject 子类的 Equality.stencil仓库还提供了一份面向 NSObject 子类的旧式相等性模板 Sourcery/Templates/Equality.stencil它基于isEqual(_:)与hash覆写而非{% for type in types.implementing.AutoEquatable|class %} extension {{ type.name }} { {{ type.accessLevel }} override func isEqual(_ object: Any?) - Bool { guard let rhs object as? {{ type.name }} else { return false } {% for variable in type.storedVariables|!annotated:skipEquality %}if self.{{ variable.name }} ! rhs.{{ variable.name }} { return false } {% endfor %} ... } } {% endfor %}该模板同样支持skipEquality注解并额外支持计算属性上的forceEquality注解强制把计算属性纳入比较与 hash。它同时会生成hash覆写适合需要在 NSObject 体系中保持一致性的场景。十二、小结Sourcery 的 AutoEquatable 模板把为类型实现 Equatable变成一行声明或一条注解结构体、类、枚举、协议按各自语义生成skipEquality控制哪些属性不参与比较arrayEquality解决协议类型数组的逐项比较可选值、多关联值枚举、访问级别等边界情况由模板与辅助函数统一处理继承与 NSObject 场景有明确的取舍与提示。在业务代码中你只需要维护 Templates/Templates/AutoEquatable.stencil 这份模板和 Templates/Tests/Context/AutoEquatable.swift 这类测试输入剩下的重复劳动全部交给 Sourcery 完成。结合 guides/Equatable.md 与仓库内的模板源码、期望输出、测试装置你可以快速把这套方案迁移到自己的项目中。赞分享代码生成开发工具【免费下载链接】SourceryMeta-programming for Swift, stop writing boilerplate code.项目地址https://gitcode.com/gh_mirrors/so/Sourcery点击查看免费下载相关推荐Sourcery 元编程实战指南用模板自动生成 Swift 样板代码Sourcery 元编程实战指南用模板自动生成 Swift 样板代码 Sourcery 是一款面向 Swift 的元编程代码生成器它扫描你的源码、应用你编写代码生成开发工具TanStack Router 文件路由命名规则pathless 布局、路由组与转义字符怎么用TanStack Router 文件路由命名规则pathless 布局、路由组与转义字符怎么用 给 TanStack Router 的文件路由目录加一个只包代码生成开发工具使用 Claudian 时哪些数据会离开本机API 请求与 Collab LAN 流量说明使用 Claudian 时哪些数据会离开本机API 请求与 Collab LAN 流量说明 Claudian 是一个把 Claude Code、Codex、G代码生成开发工具上一篇幻兽帕鲁存档迁移终极指南5分钟解决角色数据丢失问题下一篇Windows电脑运行安卓应用APK安装器完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

微信在线AI客服系统全解析:PHP+大模型API落地实战
微信在线AI客服系统全解析:PHP+大模型API落地实战

简介:一套基于PHP开发的微信在线AI客服系统源码,面向需要快速搭建724小时智能客服平台的中小企业、开发者与运维人员。系统原生对接企业微信,既能进行文本对话、图片分析和视频分析,也内置对话管理、人工转接、咨询提醒等功能&… · 2026/9/26 3:02:06

Bangumi 追番记录 App 发布流程指南:3 步完成 Android 与 iOS 双端打包上架
Bangumi 追番记录 App 发布流程指南:3 步完成 Android 与 iOS 双端打包上架

Bangumi 追番记录 App 发布流程指南:3 步完成 Android 与 iOS 双端打包上架 【免费下载链接】Bangumi :electron: An unofficial https://bgm.tv ui first app client for Android and iOS, built with React Native. 一个无广告、以爱好为驱动、不以盈利为目的、专门做 ACG 的… · 2026/9/26 3:02:00

智能图像分析与目标检测系统构建实战:卷积神经网络与边缘部署
智能图像分析与目标检测系统构建实战:卷积神经网络与边缘部署

简介:面向计算机视觉开发者与深度学习初学者的智能图像分析与目标检测系统资源包,覆盖从卷积神经网络建模、数据增强、模型优化到迁移学习、边缘计算及多模态融合的完整技术链路,适合用于智能监控、自动驾驶等场景的视觉识别原型搭建。压缩包… · 2026/9/26 3:02:00

高压直流电源电-固-热耦合仿真:用COMSOL一次算清发热、温度与热应力
高压直流电源电-固-热耦合仿真:用COMSOL一次算清发热、温度与热应力

做高压直流电源的工程师,应该都撞过这类事:样机调试完全正常,一到高温满载或者长时间老化,发现内部功率电阻的引线端子歪了,固定绝缘柱的塑料件变形了,甚至灌封的环氧树脂边缘出现细微裂纹。第一次遇到这类… · 2026/9/26 4:23:25

汽车4S店客户管理系统源码部署与改造实战解析
汽车4S店客户管理系统源码部署与改造实战解析

简介:这是一份面向计算机、软件工程等专业学生及Java Web初学者的汽车4S店客户管理系统完整源码包,适合作为课程设计、期末大作业或毕业设计的参考实现。系统围绕客户信息管理、预约服务、车辆档案等常见业务模块展开,代码采用典型分层结构&a… · 2026/9/26 4:23:19

CMake 3.26.6 Windows构建校准指南:解决MSVC/Qt/Ninja兼容性问题
CMake 3.26.6 Windows构建校准指南:解决MSVC/Qt/Ninja兼容性问题

简介:本资源为 CMake 3.26.6 官方 Windows 64 位二进制发行版安装包,面向 C 开发者、跨平台项目构建工程师及高校计算机专业学生,用于替代系统自带或旧版 CMake,解决现代 C 项目(如支持 C20/23、FetchContent、CPM 集成… · 2026/9/26 4:23:19

MCU开发必备:编译、烧录、仿真全流程解析与实战
MCU开发必备:编译、烧录、仿真全流程解析与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 4:23:19

Autosar E2E保护机制实战:从Profile选型到功能安全审核
Autosar E2E保护机制实战:从Profile选型到功能安全审核

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 4:23:19

光伏MPPT变步长扰动观察法:Simulink仿真与参数整定实战
光伏MPPT变步长扰动观察法:Simulink仿真与参数整定实战

去年调光伏控制器的MPPT程序时,我把固定步长扰动观察法的步长从0.005改到0.01,想着能追得快一点,结果稳态输出功率反而掉了2%。这个教训让我意识到,“光伏控制器MPPT”这件事里,步长恒定本身就是缺陷;随后我… · 2026/9/26 4:23:19

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码