什么是 swift macro

Swift 宏在 WWDC 2023 的 Swift 5.9 版本中引入,简单来说它允许我们在编译时生成重复代码,它还允许我们在编译之前动态地操作项目的 Swift 代码,从而允许我们在编译时注入额外的功能,使我们的应用程序的代码库更易于阅读且更高效地编码。

OC 时代的宏就是简单的代码替换,相对来说比较简单,而 Swift 宏设计的比较抽象,很难理解,但功能也更加强大,更复杂,我研究了一段时间,来尝试给大家分享一下。

swift 宏有一条原则是只能添加代码,不会删除或修改现有代码

宏的分类

按照苹果官方文档,有两种类型的宏:

1、独立宏,声明的时候使用 @freestanding 关键字,使用的时候以标签 (#) 开头,并在后边的括号里添加相应的参数,主要作用是代替代码中的内容,有点像 OC 的宏,比如官方示例中的 #stringify(a + b),或者 #warning("这是一个警告")

2、附加宏,声明的时候使用 @attached 关键字,使用的时候以 @ 开头,并在后边的括号里添加相应的参数,主要作用是为声明添加代码,比如 @OptionSet<Int>

写一个宏

1、创建宏模块

让我们跟着官方示例写一个宏,每个 Swift Macro 都是一个 Package,首先新建 Swift Macro Package,Xcode 顶部菜单 File -> New -> Package,选择 Swift Macro,下一步之后填入名称(我这里填写的名称是 MyFirstMacro),点击确认即可。

完整介绍一下 Swift 5.9 新出的宏

或者使用快捷键 shift + control + command + N

创建好之后可以看到,Xcode 自动帮我们生成了一个 Macro 的 demo,包含 4 个主要的代码文件,宏的声明、宏的实现、宏的测试 和 宏的使用

完整介绍一下 Swift 5.9 新出的宏

2、宏的声明

我们来分别解读一下这四个文件,首先是声明部分:

@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyFirstMacroMacros", type: "StringifyMacro")

在宏的分类部分我们讲到了 @freestanding 关键字用来声明独立宏,因此这个 demo 是个独立宏

freestanding 后边括号里的 expression 又是什么呢?

官方把它称为 Macro roles,也就是宏角色,不同的宏角色意味着可以实现不同功能的宏,在 WWDC 的 Session 中,Apple 对所有的角色做了说明,我这里也贴一下这张表:

同一个宏支持添加多个角色组合,比如:

@attached(accessor)
@attached(memberAttribute)
@attached(member, names: named(init()))
public macro ...

注意这个声明必须要声明 public,因为声明宏的代码与使用该宏的代码位于不同的模块

等号的后面是 #externalMacro(module: “MyFirstMacroMacros”, type: “StringifyMacro”),这是另一个独立宏,用来设定这个宏的对应的模块名是 MyFirstMacroMacros,对应的 type 是 StringifyMacro

2、宏的实现

我们打开 MyFirstMacroMacro.swift 文件,对应上边 #externalMacro 指定的模块名,这里就是宏的实现部分了。

public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression else {
            fatalError("compiler bug: the macro does not have any arguments")
        }
        return "(\(argument), \(literal: argument.description))"
    }
}

首先这里有个结构体 StringifyMacro,对应的是上边 #externalMacro 指定的 type。

StringifyMacro 遵守了 ExpressionMacro 协议,这个协议是必须遵守的,为了实现不同的宏,每种角色都有一个必须遵守的协议,下边是对照表:

我们继续分析这个 Demo 的实现,它实现了 ExpressionMacro 协议的方法:

public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax

并在其内部 return "(\(argument), \(literal: argument.description))"。要想在这个函数内做更多的事情,需要先学习 swift-syntax 这个库,其实只要创建宏模块,就会自动帮我们依赖这个库,我们的 Demo 也是如此:

完整介绍一下 Swift 5.9 新出的宏

因为这里所有语法和用到的类都是基于这个库的,我还没有好好研究,所以这里先不展开讲了,等我学完了再来分享。

简单说一下,这里其实就是返回了一个元组,元组里有两个值,第一个是 #stringify 传入的参数的值,第二个是 #stringify 传入的参数的描述

此文件最下方还有个结构体:

@main
struct MyFirstMacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
    ]
}

这段代码的作用是将我们的宏导出,如果没有这段代码也将无法正常使用。

3、宏的使用

打开 main.swift 文件,上边提到宏的声明和使用在不同模块,因此要想使用我们的宏,需要先导入

然后调用 #stringify(你的参数),Demo 里声明了两个 Int,a 和 b,调用 #stringify(a + b)

let a = 17
let b = 25
let (result, code) = #stringify(a + b)
print("The value \(result) was produced by the code \"\(code)\"")
// 打印结果:
// The value 42 was produced by the code "a + b"

可以从上边代码中看出,#stringify 返回一个元组,第一个值 result 为 a + b 的值,为 42,第二个值为参数的描述:”a + b”。

如果其他人接手你的代码,可能导致他不知道 stringify 究竟生成了什么代码,导致维护困难,Xcode 提供了一种方式可以直接将宏展开,展开之后即可看到当前这个宏生成了什么代码,选中宏之后,鼠标右键,在菜单中选择 Expand Macro

完整介绍一下 Swift 5.9 新出的宏

将宏展开之后将在这一行的下方展示展开后的代码:

完整介绍一下 Swift 5.9 新出的宏

4、宏的测试

我们来到 MyFirstMacroTests.swift 文件,这里是宏的单元测试

let testMacros: [String: Macro.Type] = [
    "stringify": StringifyMacro.self,
]
final class MyFirstMacroTests: XCTestCase {
    func testMacro() {
        assertMacroExpansion(
            """
            #stringify(a + b)
            """,
            expandedSource: """
            (a + b, "a + b")
            """,
            macros: testMacros
        )
    }
}

上边的代码调用 assertMacroExpansion 方法传入了两个字符串,第一个是宏的调用 #stringify(a + b),第二个是展开后的结果 (a + b, "a + b"),第三个参数 macros 是我们要测试的宏。

点击函数名左侧的菱形开始测试

完整介绍一下 Swift 5.9 新出的宏

之后 Xcode 会运行宏的实现部分,会拿实际返回结果和期待的展开结果做对比,如果 #stringify(a + b) 展开的结果刚好是 (a + b, "a + b"),则用例通过,否则用例失败。

5、宏的调试

要开发一个宏,避免不了需要进行调试。

调试和正常代码调试方式差不多,可以在宏的实现的部分打上断点,或者用 print 来打印,然后运行上一步的单元测试,注意一定要运行单元测试,断点和 print 才能执行

完整介绍一下 Swift 5.9 新出的宏

我们会看到,print 打印的参数是这种格式:

[SequenceExprSyntax╰─elements: ExprListSyntax  ├─[0]: IdentifierExprSyntax
  │ ╰─identifier: identifier("a")
  ├─[1]: BinaryOperatorExprSyntax
  │ ╰─operatorToken: binaryOperator("+")
  ╰─[2]: IdentifierExprSyntax
    ╰─identifier: identifier("b")]

这其实是抽象语法树(AST),也是 SwiftSyntax 的部分,关于 AST 详细的介绍之后单独出文章给大家介绍。

如果你想继续熟悉这种语法树,可以到这个在线网站使用。

最后

swift macro 是个非常强大的新功能,预计将来在项目里也会被大量使用,今天我们主要介绍的是一个很简单的示例,其实在实际的应用场景中,要远比这强大,已经有很多大佬在为开源宏努力,过几天我会再分享一下所有关于宏的学习资料和已经开源的宏列表

正如你所看到的,宏非常抽象,难以理解。如果能学会编写宏,对个人技术成长来说绝对是件好事。

大家如果第一遍不能看懂,可以先把这篇文章收藏,然后慢慢开始实践,再回头看这篇文章,相信你会有新的收获

也可以把我的公众号设置为星标,这样以后有新的相关资料,会第一时间通知到你。

参考资料

[1]

Swift AST Explorer: swift-ast-explorer.com/

点击下方公众号卡片,关注我,每天分享一个关于 iOS 的新知识

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!