Swift 宏(Macro)入门趣谈(二)
概述
苹果在去年 WWDC 23 中就为 Swift 语言新增了“其利断金”的重要小伙伴 Swift 宏(Swift Macro)。为此,苹果特地用 2 段视频(入门和进阶)颇为隆重的介绍了它。
那么到底 Swift 宏是什么?有什么用?它和 C/C++ 语言中的宏又有什么异同呢?本系列博文将会尝试为小伙伴们揭开 Swift 宏的神秘面纱。
在本篇博文中,您将学到如下内容:
- 概述
- 3. Swift 宏的种类和应用场景
- 3.1 独立宏(Freestanding macros)
- 3.2 附属宏(Attached macros)
- 3.3 适用场景
- 总结
相信学完本系列博文后,Swift Macro 会从大家心中的“阳春白雪”变为“阳阿薤露”,小伙伴们必可以将它们运用的“如臂使指”。
那还等什么呢?Let‘s go!!!😉
3. Swift 宏的种类和应用场景
为了细粒度和规范化 Swift 宏的使用场景,苹果将其分为两大类,7 小类,它们分别是:
- Freestanding macros(独立宏)
- expression
- declaration
- Attached macros(附属宏)
- peer
- accessor
- memberAttribute
- member
- conformance
在 WWDC 23 视频中苹果也将这些宏类型称之为宏角色(macor roles):
简单来说,独立宏(Freestanding macros)自身会单独出现,并不附在声明(declaration)上;而附属宏(Attached macros)会改变它所附属的声明。
3.1 独立宏(Freestanding macros)
对于独立宏,我们可以用苹果默认宏模版例子中的 stringify 宏来诠释一下它:
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroMacros", type: "StringifyMacro")
如上所示 stringify 即为一个独立宏,它的作用是根据输入内容生成新的表达式(expression)。我们可以这样使用它:
let a = 17
let b = 25
let (result, code) = #stringify(a + b)
print("The value \(result) was produced by the code \"\(code)\"")
如下图所示,我们实际将加法表达式转换为了一个元组表达式,我们还可以随时展开源代码中的宏来检查结果是否符合预期:
3.2 附属宏(Attached macros)
相对于独立宏,附属宏更像依赖于现有内容的“精灵”,它会根据所附属声明的内容生成相关联的新代码。比如,它可以为现有类型新增构造器、方法和属性。
举一个苹果官方的“栗子”,假设我们需要自动生成类似下面的 OptionSet 类型:
struct SundaeToppings: OptionSet {
let rawValue: Int
static let nuts = SundaeToppings(rawValue: 1 << 0)
static let cherry = SundaeToppings(rawValue: 1 << 1)
static let fudge = SundaeToppings(rawValue: 1 << 2)
}
我们可以创建一个如下的 OptionSet<RawType> 宏:
@attached(member)
@attached(extension, conformances: OptionSet)
public macro OptionSet<RawType>() =
#externalMacro(module: "SwiftMacros", type: "OptionSetMacro")
有了它,我们就可以轻松的让任意类型变成符合条件的 OptionSet 啦:
@OptionSet<Int>
struct SundaeToppings {
private enum Options: Int {
case nuts
case cherry
case fudge
}
}
@OptionSet 会为上面的 SundaeToppings 结构自动生成所需的代码从而满足功能需求:
struct SundaeToppings {
private enum Options: Int {
case nuts
case cherry
case fudge
}
// 下面的代码都是宏自动生成的
typealias RawValue = Int
var rawValue: RawValue
init() { self.rawValue = 0 }
init(rawValue: RawValue) { self.rawValue = rawValue }
static let nuts: Self = Self(rawValue: 1 << Options.nuts.rawValue)
static let cherry: Self = Self(rawValue: 1 << Options.cherry.rawValue)
static let fudge: Self = Self(rawValue: 1 << Options.fudge.rawValue)
}
// 同样是宏生成的代码
extension SundaeToppings: OptionSet { }
由此可见,作为附属宏的 @OptionSet 附着在 SundaeToppings 结构上,并让它“脱胎换骨”。
由于篇幅有限,这里仅介绍 Swift 宏两种大类型的使用情况,至于每种小类型(Freestanding macros 中的 peer 小类型会在后面详述)的详细介绍请小伙伴们参考官方文档、github 示例以及我后续的博文。
3.3 适用场景
从上面的介绍可知,Swift Macros 是一种编译器在编译时生成代码的静态工具。这意味着,我们最好用它来做与现有代码相关的事而不是“天马行空”般脑洞大开的反其道而行之。
它操作的应该是编译时能确定的内容,比如方法名或可能出现的警告:
func myFunction() {
print("Currently running \(#function)")
#warning("Something's wrong")
}
不要(也不应该)在宏扩展时根据外部动态内容生成代码,比如:从网络(别忘了沙盒限制)实时下载数据、或是返回当前的时间(Date.now)等。
在了解了 Swift 宏的种类和适用场景之后,我们将在下一篇博文中来聊聊 Swift 宏代码的组织结构,敬请期待吧。
总结
在本篇博文中,我们介绍了 Swift 宏的种类(2大类,7小类)和实际编码中应该遵循的适用准则。
感谢观赏,下篇再会!😎