swift12345678func foo(_ bar: Int?) { for _ in 0..<10 { #unwrap(bar) { print(bar) } } // 其余代码}
展开之后:
swift1234567891011func foo(_ bar: Int?) { for _ in 0..<10 { // #unwrap 宏展开开始 guard let bar = bar else { return } print(bar) // #unwrap 宏展开结束 } // 其余代码}
通过上面的宏展开,我们可以知道如果将空值传递给 foo,则 for 循环后的其余代码不会被执行。这是因为宏展开引入的 return 语句会中断外部循环。然而,#unwrap 宏的名称只传达了对可选值解包的目的。这可能会让使用这个宏的程序员认为从应用点返回是一个意外行为。
swift12345678func foo(_ bar: Int?) { // #unwrap 宏展开开始 guard let bar = bar else { return } print(bar) // #unwrap 宏展开结束}
这个宏展开引入了变量名 shadowing,其中 guard let bar: Int shadow 了参数 _ bar: Int?。对于 #unwrap 来说,变量名 shadowing 是无足轻重的,因为这是一种有意的行为。然而,由于 freestanding Swift macro 的宏展开涉及应用点的词法作用域共享,这使得一些朴素实现的宏展开可能会使得变量名 shadowing 变成变量名重复声明。以下是一个人造的例子:由于宏展开,变量名 updater 从被 shadow 变成被重复声明。展开之前:
swift123456789func foo(_ bar: Int?) { let updater = Updater() #unwrap(bar) { let updater = Updater() print(bar) updater.update() } print(updater.description)}
展开之后:
swift1234567891011121314func foo(_ bar: Int?) { // `updater` 第一次被声明 let updater = Updater() // #unwrap 宏展开开始 guard let bar else { return } // `updater` 第二次被声明 let updater = Updater() print(bar) updater.update() // #unwrap 宏展开结束 print(updater.description)}
swift12345678910111213@COW@DictionaryLikestruct User { // 其他内容 ... // 由 @COW 宏展开引入 var _$storage: Storage // 由 @DictionaryLike 宏展开引入 var _$storage: [String : Any] = [:]}
显然,这在 Swift 中无法编译,因为 Swift 不允许属性重载。在这种情况下,我们会再次得到 "invalid redeclaration of a variable" 错误。
由于 @DictionaryLike 宏展开而导致的变量重复声明从而引发的编译失败
1.4唯一语言结构的命名冲突
在 Swift 中,一些语言结构在其父一级结构下是唯一的。这意味着当多个宏尝试在相同的父一级结构中生成相同的子结构时,代码会变得无法编译。例如,属性声明中的 get 和 set accessor —— 如果我们在属性声明中添加多个 get accessor,那么代码将无法编译。这个错误一般很少在手写代码中发生,但在使用宏时需要引起注意。我们可以从之前的 @DictionaryLike 示例开始深入了解这个问题。假设有一个 accessor macro 叫做 @UseDictionaryStorage,它为所附着的属性生成 get 和 set accessor。getter 和 setter 将访问转发到由 @DictionaryLike 宏展开带来的存储容器中。宏展开之前:
swift12345678@COW@DictionaryLikestruct User { @UseDictionaryStorage var info: [String : Any]?}
宏展开之后:
swift1234567891011121314151617@COW@DictionaryLikestruct User { @UseDictionaryStorage var info: [String : Any]? { // @UseDictionaryStorage 宏展开开始 get { return _$storage["info"] as? [String : Any]? } set { _$storage["info"] = newValue } // @UseDictionaryStorage 宏展开结束 }}
然而,这过度简化了所发生的事情。@COW 宏的真实展开结果是:
swift123456789101112131415161718192021222324252627@COW@DictionaryLikestruct User { @UseDictionaryStorage @COWIncluded var info: [String : Any]? { // @COWIncluded 宏展开开始 get { _$storage.info } set { makeStorageUniqueIfNeeded() _$storage.info = newValue } // @COWIncluded 宏展开结束 // @UseDictionaryStorage 宏展开开始 get { return _$storage["info"] as? [String : Any]? } set { _$storage["info"] = newValue } // @UseDictionaryStorage 宏展开结束 }}
我们可以观察到,在 info 属性下生成了两个 get 和 set accessor。由于 Swift 的语法只允许一个属性中有一个 get/set accessor,这个展开会导致 Swift 中的语法错误,最终使得代码无法编译。然而,这还是没有看到全貌。通过应用生产级实现的 COW 宏,我们可以看到 get 和 set accessor 被优化为了 _read 和 _modify 以生产环境中提供更好的性能,同时我们还可以观察到 Swift 不仅禁止程序员定义名称相同的多个 accessor 而且还禁止定义多个语义相同的 accessor。
通过 @UseDictionaryStorage 宏展开产生的多重 accessor 从而引起的编译错误
swift123456789101112131415161718@propertyWrapperstruct Capitalized { var wrappedValue: String { didSet { wrappedValue = wrappedValue.capitalized } }}@COWstruct User { @Capitalized var name: String = ""}
展开之后的代码:
swift123456789101112131415161718@COWstruct User { @COWIncluded @Capitalized var name: String = "" { get { return _$storage.name } set { makeStorageUniqueIfNeeded() _$storage.name = newValue } } // ...}
我们得到这个展开结果是因为 property wrapper 的「展开」发生在宏展开之后。根据这个结果,@Capitalized property wrapper 仍然附着在 name 变量上,但由于宏展开,变量从存储属性变成了计算属性。最终,我们会得到编译器诊断出来的错误:
Property wrapper cannot be applied to a computed property
Property wrapper cannot be applied to a computed property.
这不仅限于 property wrappers,lazy 关键字也可能导致进入同样的死胡同。
swift123456@COWclass User { lazy var name: String = { "Jane Doe" }()}
swift1234567891011121314151617@COWclass User { @COWIncluded lazy var name: String = { "Jane Doe" }() { get { return _$storage.name } set { makeStorageUniqueIfNeeded() _$storage.name = newValue } } // ...}
Lazy cannot be applied to a computed property.
通过这些例子,我们可以了解到 Swift 宏的展开可能会改变源代码的语义。这可能会导致语义冲突,最终使得展开结果无法通过编译。
2解决方案:渐进式控制宏展开
到目前为止,我们已经列出了一系列在实现 Swift 宏时可能会遇到的典型陷阱和缺陷。乍一看,这个列表可能让你感到不知所措。然而,我发明了一种简单的方法来摆脱这些问题:渐进式控制宏展开。这个方法的思想源自于「渐进式暴露 API 设计」和「渐进式类型化」,并借鉴了从苹果 Swift Observation 和 SwiftData 的实现中提取出的一些思路。「渐进式控制宏展开」的思想非常简单:如果没有冲突,那么程序员就不需要费力绕过冲突解决机制;如果有,那么我们必须有工具使得程序员可以用最小的努力解决冲突。
2.1最大化成为幸运儿的概率
如果一个程序员在使用 Swift 宏的过程中不需要解决任何冲突,那么我们可以说该程序员成为了幸运儿。为了最大化 Swift 宏用户获得预期结果的可能性,我们必须遵守以下几点:
条目 1:操作控制流的宏应该具有反映这一目的的名称。
这个项目的目的是为了避免对之前提到的 #unwrap 宏的误用:
swift1234567func foo(_ bar: Int?) { for _ in 0..<10 { #unwrap(bar) { print(bar) } }}
它被展开为:
swift12345678910func foo(_ bar: Int?) { for _ in 0..<10 { // #unwrap 宏展开开始 guard let bar = bar else { return } print(bar) // #unwrap 宏展开结束 }}
然而,我们不能保证程序员总是会遇到幸运的情况。肯定会有一些情况需要程序员自己解决冲突。通过应用上述条目,我们仍然可能会遇到潜在的名称和语义冲突。我们唯一能做的就是面对并解决它们:因为我们不能假设一个宏作者能够预测到其他所有宏作者可能选择的名称,同时,不变的语义绝对不应该是 Swift 宏展开的性质,因为这会降低 Swift 宏的灵活性。理想的情况是拥有一组解决冲突的工具,然后这些工具在使用成本方面呈现出平滑曲线。这些工具分别是: