Swift 宏(Macro)入门趣谈(一)
概述
苹果在去年 WWDC 23 中就为 Swift 语言新增了“其利断金”的重要小伙伴 Swift 宏(Swift Macro)。为此,苹果特地用 2 段视频(入门和进阶)颇为隆重的介绍了它。
那么到底 Swift 宏是什么?有什么用?它和 C/C++ 语言中的宏又有什么异同呢?本系列博文将会尝试为小伙伴们揭开 Swift 宏的神秘面纱。
在本篇博文中,您将学到如下内容:
- 概述
- 1. 从一个”尴尬“的小例子聊起
- 2. 什么是 Swift 宏?它与其它语言中的宏有何不同?
- 总结
相信学完本系列博文后,Swift Macro 会从大家心中的“阳春白雪”变为“阳阿薤露”,小伙伴们必可以将它们运用的“如臂使指”。
那还等什么呢?Let‘s go!!!😉
1. 从一个”尴尬“的小例子聊起
其实早在去年苹果祭出 Swift 5.9 时,Swift 语言本身就已经加入了超多先进和现代化的特性。
自从今年 Swift 大版本进化到 6 之后,几乎所有秃头码农们都不约而同的认为 Swift 不仅已经前所未有地成熟和稳定,而且它的能力也变得史无前例的强大。真可谓:“多平台,台台出彩;多系统;统统有戏”,貌似 Swift 已经变得“登峰造极”、“气吞山河”了。
果真如此吗?
更多关于 Swift 5.9 和 Swift 6.0 语言的全方位介绍,请小伙伴们移步我的《Swift 语言开发精讲》专栏和苹果 WWDC 24 官方站点观赏精彩的文章和视频:
- 《Swift 语言开发精讲》
- WWDC 2024
咱先别把话说满,虽说 Swift 现今已足够强大,但它真的是无所不能,丝毫不存在所谓的“阿格硫斯之踵”吗?
答案是否定的!下面我们就举一个小“栗子”让看似强大的 Swift “一秒破功”。
假设我们要实现一个很简单的功能:按类型实例的属性排序。
struct Item {let name: Stringlet nickname: String?let age: Intlet power: Double
}
在上面的 Item 中存在若干属性,我们希望写一个通用的排序方法对 Item 集合排序:
struct Model {let items: [Item]func sortItemsBy<Value: Comparable>(keyPath: KeyPath<Item, Value>, sortOrder: SortOrder = .forward) throws -> [Item] {items.sorted(using: SortDescriptor(keyPath, order: sortOrder))}
}
如上代码所示,我们在模型 Model 中创建了一个 sortItemsBy() 方法按 Item 中任意属性来排序 [Item] 数组。
比如,我们可以分别依据 Item 的 name 和 age 属性来生成不同排序后的数组:
let model = Model(items: [])
let itemsByName = try! model.sortItemsBy(keyPath: \.name)
let itemsByAge = try! model.sortItemsBy(keyPath: \.age, sortOrder: .reverse)
这样很好很强大,不过目前排序功能有点小问题:就是我们无法排序类型为可选值的属性:
let itmesByNickname = try! model.sortItemsBy(keyPath: \.nickname)
如您所见,当我们试图按 nickname 属性排序 Item 数组时,编译器就会大声抱怨。原因前面已经说了,因为 nickname 属性的类型是 String?(即 Optional<String>):
对此,我们可以提出相应的解决方法,即另外写一个可选(Optional)参数版本的排序方法:
func sortItemsBy<Value: Comparable>(keyPath: KeyPath<Item, Value?>, sortOrder: SortOrder = .forward) throws -> [Item] {items.sorted(using: SortDescriptor(keyPath, order: sortOrder))
}
这个方法与之前的几乎一毛一样,唯一不同之处在于 KeyPath 的 Value 是可选类型。
现在我们就有点“哭笑不得”了,对于排序功能我们竟然要写两个方法,即使它们几乎如出一辙。
当然对于这样简单的排序功能,我们完全可以想办法“剥离”外部的方法而直接使用 [Item] 上的 sorted() 方法。
有的小伙伴们可能会想出下面这种“左右逢源”的排序实现:
func sortItemBy<Value: Comparable>(keyPath1: KeyPath<Item, Value>?, keyPath2: KeyPath<Item, Value?>?, sortOrder: SortOrder = .forward) throws -> [Item] {if let keyPath1 {items.sorted(using: SortDescriptor(keyPath1, order: sortOrder))} else if let keyPath2 {items.sorted(using: SortDescriptor(keyPath2, order: sortOrder))} else {throw MyError("至少要有一个 KeyPath 不为 nil!")}
}
但是这样一来,我们仍然在方法内部造成了代码重复并且整体实现根本毫无优雅性可言。
与此类似的场景在偏静态的 Swift 语言中,绝对会让我们心余力绌,坐如针毡。这就是 Swift 缺乏的能力:用代码生成代码!
一般来说,我们有两种办法来解决此事:
- 在运行时动态生成代码,ruby 和 Python 就非常精于此道(所以 ruby 和 Python 之类的语言根本不需要所谓的宏);
- 由编译器(预处理器)当家做主在编译前生成代码,C/C++ 宏的强项;
将来有没有暂且不说,目前 Swift 还没有 ruby 和 Python 那种运行时动态“造码”的能力。不过从 WWDC 23 那年开始,苹果推出了 Swift 宏专注于在编译时静态“造码”。
2. 什么是 Swift 宏?它与其它语言中的宏有何不同?
在官方的开发文档中,苹果就为 Macro 的特性定了基调:在编译时生成代码。 具体来说,Swift Macro 在编译时可以根据现有代码转换或生成新代码,这样做的最大好处就是避免重复(DRY)
Swift 编译器会在我们的逻辑代码编译前将其中的任何宏展开(Expands)。Swift 宏有两个重要的特性:
- 它绝不会删除已存在的代码;
- 它绝不会修改(其它)已存在的代码;
其实,在 WWDC 23 之前苹果就已经在代码中使用过海量的特殊宏,从最常见的 #function、#file、#error 宏,到 @main 、@available、@discardableResult 宏等等。我感觉在 WWDC 23 之前的某个时刻,苹果一定就有把这些特殊宏升级为通用宏的壮志凌云。
关于 Swift 宏的详细文档,大家可以到苹果官方开发站点一窥究竟:
- Swift Macros
虽然都是“宏字辈”,但是 Swift 中的宏还是与 C/C++ 等语言中的宏有些区别的:
- Swift 中的宏完全参与到 Swift 的类型系统中,可以在编译时保证类型安全;
- 苹果确保宏的展开过程是一个沙箱(Sandbox)操作,不会带来安全漏洞或泄露系统中用户的隐私;
- 在宏展开发生错误时,Swift Macros 提供了更多的机制来帮助用户快速了解错误原因和解决办法;
- 我们可以为宏编写单元测试,增强它的鲁棒性;
因为 C/C++ 中的宏只是一个简单的字符串字面值替换器,所以无论是用户编写和编译器支持起来都不算太难,但是 Swift 的宏就完全是另一回事了。
想要系统学习 Swift 的小伙伴们,欢迎来我的 《Swift语言开发精讲》专栏逛一逛哦。
- 《Swift 语言开发精讲》
在了解了 Swift 宏之后,在下一篇博文中我们就来看看它能做些什么以及不要用它们做什么?
总结
在本篇博文中,我们讨论了 Swift 宏的基本概念,以及它与 C/C++ 语言中的宏有何不同。
感谢观赏,我们下一篇见!😎