Expand on Swift macros
Swift & UI 进阶 20m

深入 Swift 宏:角色、展开与实现

Expand on Swift macros

2023年6月5日

在 Apple 官方观看视频

一句话判断

Swift 宏不是 C 预处理器的字符串替换,而是一套有类型检查、角色约束和沙盒隔离的编译器插件系统。

这场 Session 讲了什么

Becca 从 Swift 团队的角度系统讲解了 Swift 宏的设计理念和实现机制:

为什么需要宏。Swift 本身有很多自动生成代码的特性(如 Codable、Result Builder),但这些是硬编码在编译器中的。宏让你可以创建自己的”语言特性”,以 Swift Package 的形式分发,不需要修改编译器。

四大设计目标。一看就知道是宏(# 开头的独立宏,@ 开头的附属宏);输入输出都经过类型检查;展开只会添加代码不会删除或修改;不是黑魔法——Xcode 中可以右键查看展开结果、设断点、单步调试。

角色(Role)系统。这是宏的核心约束机制。独立宏有 expression(展开为表达式)和 declaration(展开为声明)两种角色。附属宏有五种:peer(添加新的声明)、accessor(为存储属性添加 getter/setter)、memberAttribute(为成员添加属性)、member(添加新成员)、conformance(添加协议遵循)。

实现机制。宏作为独立的编译器插件运行在安全沙盒中。当编译器遇到宏调用时,提取相关代码发送给插件,插件返回展开后的代码,编译器将其加入程序一起编译。

值得深挖的点

角色决定了展开的插入位置peer 角色在原声明旁边添加新声明;memberAttribute 为已有成员添加属性标注;member 添加全新的成员;accessor 把存储属性变成计算属性;conformance 添加协议扩展。理解每种角色的规则是正确设计宏的前提。

安全性保障。宏运行在独立进程中,通过沙盒隔离。它只能看到编译器传递给它的语法树片段,不能访问文件系统、网络或进程状态。这种设计让宏的”可添加不可删除”约束得以执行——编译器检查展开结果,确保它只添加了代码。

可调试性。即便宏来自闭源库,你也能在 Xcode 中查看展开结果、设断点、调试。编译错误会同时标注展开代码中的位置和原始代码中的位置。宏作者还可以为宏编写单元测试。

代码片段

// 独立表达式宏:更安全的强制解包
// 声明
@freestanding(expression)
macro assertNotNil<T>(_ value: T?, message: String) -> T

// 使用 — 编译器类型检查确保参数完整
let name: String? = "张三"
let unwrapped = #assertNotNil(name, message: "名字不能为空")
// 展开为类似这样的代码:
// guard let unwrapped = name else {
//     fatalError("名字不能为空")
// }
// return unwrapped
// 附属宏:peer 角色示例
// 自动为结构体生成 Equatable 的 == 方法
@attached(peer)
macro AutoEquatable()

// 使用
@AutoEquatable
struct Point {
    var x: Double
    var y: Double
    // 宏会在旁边生成:
    // static func == (lhs: Point, rhs: Point) -> Bool {
    //     lhs.x == rhs.x && lhs.y == rhs.y
    // }
}
// 附属宏:member 角色示例
// 为 Codable 结构体自动添加 CodingKeys
@attached(member)
macro AutoCodable()

@AutoCodable
struct User {
    var name: String
    var age: Int
    // 宏自动生成 CodingKeys 枚举和 init(from:) / encode(to:)
}

最佳实践

  • 宏的定义和实现要分开:定义提供 API(声明),实现在编译器插件中。
  • 选择正确的角色:需要添加新成员用 member,需要修改成员的属性用 memberAttribute
  • 为宏编写单元测试,使用 Swift Testing 框架验证展开结果。
  • 展开结果要尽量简洁,不要生成过多的中间代码。
  • 如果数据可以通过属性或构造器直接传递,就不要用宏——宏适合消除真正的样板代码。

还有什么值得关注

  • Xcode 的宏展开查看功能:右键宏调用 -> “Expand Macro”
  • Swift Package 中宏的配置需要声明编译器插件 target
  • Codable 的自动实现就是一个内置的”宏”概念,宏让这种能力变得可扩展
  • 宏的作者要特别注意诊断信息的质量,帮助使用者理解错误
WWDC 2023