开始写一个 Swift 宏吧

什么是宏

Apple 在 Swift 5.9 里面加入了 Swift macros(宏),宏可以在编译的过程中帮我们生成一些需要重复编写的代码。WWDC 23 中有两个关于宏的 Session,Expand on Swift macros 介绍了什么是宏和宏的几种类型,Write Swift macros 介绍了怎么去写一个宏。这两个 Session 介绍了每种宏可以做什么,但是缺少了详细的代码,我不知道具体要怎么去实现我想要的效果,在查阅了一些资料Swift 官方库的内部实现之后才知道每个宏的定义和用法。

宏类型介绍

宏主要分为两种类型:

@freestanding 是一个独立的宏(与 # 语法一起使用),并且可以用作表达式。

@attached 是一个附加宏(与 @ 语法一起使用),需要搭配 struct/class/enum/property/function 等类型使用,可以为其添加代码。

每个类型的宏具体能干什么?

@freestanding(expression)

编写一段代码使其返回一个值。

let url = #URL("https://www.baidu.com")
// 宏内部会判断该字符串能否生成 URL,如果无法生成会报错,将运行报错提前到了编译阶段。
let url = #URL("https:// www.baidu.com") // 报错:UnableToCreateURL

宏生成代码:

let url = URL(string: "https://www.baidu.com")!

<details>
<summary>宏的实现代码(点击查看)</summary>

/// 声明
@freestanding(expression)
public macro URL(_ value: String) -> URL = #externalMacro(module: "MyMacroMacros", type: "URLMacro")

/// 实现
public struct URLMacro: ExpressionMacro {
    enum MacroError: Error {
        case unableToCreateURL
    }
    
    public static func expansion<Node: FreestandingMacroExpansionSyntax, Context: MacroExpansionContext>(
        of node: Node,
        in context: Context
    ) throws -> ExprSyntax {
        let content = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self)?.segments.first?.description ?? ""
        guard let _ = URL(string: content) else {
            throw MacroError.unableToCreateURL // 无法生成 URL,报错
        }
        return "URL(string: \"\(raw: content)\")!"
    }
}

</details>

@freestanding(declaration)

宏可以写在任意地方,可以创建一段或多段代码。

#guardValue(self)

宏生成代码:

guard let self = self else { return }

<details>
<summary>宏的实现代码(点击查看)</summary>

/// 声明
@freestanding(declaration)
public macro guardValue(_ values: Any...) = #externalMacro(module: "MyMacroMacros", type: "GuardMacro")

/// 实现
public struct GuardMacro: DeclarationMacro {
    public static func expansion<Node: FreestandingMacroExpansionSyntax, Context: MacroExpansionContext>(
        of node: Node,
        in context: Context
    ) throws -> [DeclSyntax] {
        let code = node.argumentList.map {
            $0.expression.description
        }.map {
            "let \($0) = \($0)"
        }.joined(separator: ", ")
        return [
            "guard \(raw: code) else { return }"
        ]
    }
}

</details>

@attached(peer)

宏会在同个代码层级生成一段代码。

@AddCompletionHandler()
func fetchDetail(_ id: Int) async -> String? { }

宏生成代码:

// 宏会在同个代码层级生成代码
func fetchDetail(_ id: Int, completionHandler: @escaping (String?) -> Void) {
  Task {
    completionHandler(await fetchDetail(id))
  }
}

<details>
<summary>宏的实现代码(点击查看)</summary>

该宏来自 Swift 官方库
声明
实现

</details>

目前在 beta 1 中生成出来的代码无法直接被调用,不清楚是否是宏写的有问题,还是有 Bug。我更倾向这是 Bug,上面提到的 #guardValue 宏也无法调用到解包后的变量。如果是我用法的问题,麻烦在评论区告诉我。

@attached(accessor)

可以给变量生成 get、set、willSet、didSet 等方法。

class Foo {
    @PrintWhenAssigned
    var name: String = ""
}

let f = Foo()
f.name = "Tom" // Logs: Tom
f.name = "Bob" // Logs: Bob

宏生成代码:

class Foo {
    @PrintWhenAssigned
    var name: String = ""
    {
        didSet {
            print(name)
        }
    }
}

<details>
<summary>宏的实现代码(点击查看)</summary>

/// 声明
@attached(accessor)
public macro PrintWhenAssigned() = #externalMacro(module: "NetworkMacros", type: "PrintWhenAssignedMacro")

/// 实现
public struct PrintWhenAssignedMacro: AccessorMacro {
    
    public static func expansion<Context: MacroExpansionContext, Declaration: DeclSyntaxProtocol>(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: Declaration,
        in context: Context
    ) throws -> [AccessorDeclSyntax] {
        guard let propertyName = declaration.as(VariableDeclSyntax.self)?.bindings.first?.pattern.description else { return [] }
        return [
        """
        didSet {
            print(\(raw: propertyName))
        }
        """
        ]
    }
}

</details>

@attached(memberAttribute)

可以给 struct/class/enum 等里面的属性、方法加上 attribute,比如 @property、宏 等。

@TestMemberAttribute
public class Foo {
    var name: String = ""
    func foo() { }
}

宏生成代码:

@TestMemberAttribute
public class Foo {
    @SomeMacro
    var name: String = ""
    @SomeMacro
    func foo() { }
}

<details>
<summary>宏的实现代码(点击查看)</summary>

/// 声明
@attached(memberAttribute)
public macro TestMemberAttribute() = #externalMacro(module: "MyMacroMacros", type: "TestMemberAttributeMacro")

/// 实现
public struct TestMemberAttributeMacro: MemberAttributeMacro {
    
    public static func expansion<Declaration: DeclGroupSyntax, MemberDeclaration: DeclSyntaxProtocol, Context: MacroExpansionContext>(
        of node: AttributeSyntax,
        attachedTo declaration: Declaration,
        providingAttributesFor member: MemberDeclaration,
        in context: Context
    ) throws -> [AttributeSyntax]  {
        return ["@SomeMacro"]
    }
}

</details>

@attached(member)

可以给 struct/class/enum 添加属性、方法。

@CaseDetection
enum Animal {
    case cat(String)
}

宏生成代码:

@CaseDetection
enum Animal {
    case cat(String)
  
    var isCat: Bool {
        if case .cat = self { true }
        else { false }
    }
}

宏的实现代码在后面的案例中。

@attached(conformance)

可以给 struct/class 添加协议和约束。

@TestConformance
struct Foo { }

宏生成代码:

extension Foo : SomeProtocol where AAA: BBB {}

<details>
<summary>宏的实现代码(点击查看)</summary>

/// 声明
@attached(conformance)
public macro TestConformance() = #externalMacro(module: "MyMacroMacros", type: "TestConformanceMacro")

/// 实现
public struct TestConformanceMacro: ConformanceMacro {

    public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>(
        of node: AttributeSyntax,
        providingConformancesOf declaration: Declaration,
        in context: Context
    ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
        let conformance = try GenericWhereClauseSyntax(
            leadingTrivia: .newline,
            requirementList: [
                .init(body: .conformanceRequirement(.init(
                    leftTypeIdentifier: TypeSyntax(stringLiteral: " AAA"),
                    rightTypeIdentifier: TypeSyntax(stringLiteral: " BBB"))))
            ])
        return [("SomeProtocol", conformance)]
    }
}

</details>

怎么自己创建宏

写宏的准备工作

1.创建工程

新建一个 Swift Macro Package ,Xcode -> File -> New -> Package,选择 Swift Macro

Swift Macro 需要依赖 apple/swift-syntax 第三方库,这是 Apple 的词法分析库,用于解析、检查、生成和转换 Swift 源代码。

创建完成后,我们可以看到项目的结构是这样的:

├── Package.resolved
├── Package.swift
├── Sources
│   ├── MyMacro
│   │   └── MyMacro.swift // 宏声明文件
│   ├── MyMacroClient
│   │   └── main.swift // 可运行文件,可以在这里测试宏的实际效果
│   └── MyMacroMacros
│       └── MyMacroMacro.swift // 宏实现文件
└── Tests
    └── MyMacroTests
        └── MyMacroTests.swift // 宏测试文件,用于编写、调试宏
2.宏实现文件

我们先打开 MyMacroMacro.swift 写一下上面提到的 @CaseDetection 宏。先让宏遵守 MemberMacro 协议,然后点击报错让 Xcode 生成协议方法,生成之后先返回一个空数据,并将断点打到 return [] 上面,不着急写宏。

public struct CaseDetectionMacro { }

extension CaseDetectionMacro: MemberMacro {
    
    public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>(
        of node: AttributeSyntax,
        providingMembersOf declaration: Declaration,
        in context: Context
    ) throws -> [DeclSyntax] {
        return []
    }
}

然后我们需要在底部将宏加到 MyMacroPlugin 里面。

@main
struct MyMacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
        CaseDetectionMacro.self,
    ]
}
3.宏声明文件

打开 MyMacro.swift 文件声明一下宏:

// 如果宏遵守了多个协议,需要在这里写上多个 @attched()
@attached(member)
public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro")
4.宏测试文件

打开 MyMacroTests.swift 文件写一个测试用例,目的是为了能断点打到宏里面。

先在 testMacros 里面加上我们的宏:

let testMacros: [String: Macro.Type] = [
    "stringify": StringifyMacro.self,
    "CaseDetection": CaseDetectionMacro.self,
]

再写一个测试用例,这里 expandedSource 是宏预期生成出来的代码,我们可以先不写。

func testCaseDetectionMacro() {
    assertMacroExpansion(
        """
        @CaseDetection
        enum Animal {
            case cat
        }
        """,
        expandedSource: """
        """,
        macros: testMacros
    )
}

运行测试用例,我们就会进入宏实现的断点里面了,这时候我们可以开始写宏了。

开始写宏

public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>(
    of node: AttributeSyntax,
    providingMembersOf declaration: Declaration,
    in context: Context
) throws -> [DeclSyntax] {
    return []
}
node

node 参数可以获取宏的声明部分,如果宏接收参数可以从 node 中取到,执行 po node

AttributeSyntax
├─atSignToken: atSign
╰─attributeName: SimpleTypeIdentifierSyntax
  ╰─name: identifier("CaseDetection")

如果我们想要获取宏的名称可以这样写:

let macroName = node.attributeName.description // "CaseDetection"
declaration

declaration 参数可以获取类型里面的定义,执行 po declaration

EnumDeclSyntax
├─attributes: AttributeListSyntax
│ ╰─[0]: AttributeSyntax
│   ├─atSignToken: atSign
│   ╰─attributeName: SimpleTypeIdentifierSyntax
│     ╰─name: identifier("CaseDetection")
├─enumKeyword: keyword(SwiftSyntax.Keyword.enum)
├─identifier: identifier("Animal")
╰─memberBlock: MemberDeclBlockSyntax
  ├─leftBrace: leftBrace
  ├─members: MemberDeclListSyntax
  │ ╰─[0]: MemberDeclListItemSyntax
  │   ╰─decl: EnumCaseDeclSyntax
  │     ├─caseKeyword: keyword(SwiftSyntax.Keyword.case)
  │     ╰─elements: EnumCaseElementListSyntax
  │       ╰─[0]: EnumCaseElementSyntax
  │         ╰─identifier: identifier("cat")
  ╰─rightBrace: rightBrace
调试

宏需要获取枚举的名称,我们现在断点里面获取到想要的数据,再去写代码。

我们一步步去点开,会发现到 decl 就下不去了。

po declaration.memberBlock.members.first!.decl

因为 decl 是顶层的协议 DeclSyntax,我们需要使用 as() 将其转换为 EnumCaseDeclSyntax

po declaration.memberBlock.members.first!.decl.as(EnumCaseDeclSyntax.self)

在写宏的过程中,我们会经常遇到这个问题,发现类型对不上可以用 as() 进行类型转换,最终的调试代码:

po declaration.memberBlock.members.first!.decl.as(EnumCaseDeclSyntax.self)?.elements.first!.identifier.description // "cat"
宏实现代码

根据这个调试代码,我们可以去写宏实现代码了。

public struct CaseDetectionMacro { }

extension CaseDetectionMacro: MemberMacro {
    
    public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>(
        of node: AttributeSyntax,
        providingMembersOf declaration: Declaration,
        in context: Context
    ) throws -> [DeclSyntax] {
        var names: [String] = []
        for member in declaration.memberBlock.members { // 循环获取所有属性、方法
            let elements = member.decl.as(EnumCaseDeclSyntax.self)?.elements
            if let propertyName = elements?.first?.identifier.description {
                names.append(propertyName) // 取出枚举名
            }
        }
        
        return names.map { // 拼接实现代码
            """
            var \("is" + capitalized($0)): Bool {
                if case .\($0) = self { true }
                else { false }
            }
            """
        }.map {
            DeclSyntax(stringLiteral: $0)
        }
    }
    
    /// 首字母大写
    private static func capitalized(_ str: String) -> String {
        var str = str
        let firstChar = String(str.prefix(1)).uppercased()
        str.replaceSubrange(...str.startIndex, with: firstChar)
        return str
    }
}
查看宏效果

最后我们到 main.swift 里面写一个枚举测试一下宏。

@CaseDetection
enum Animal {
    case cat
}

写完我们可以右击 @CaseDetection 宏,点击 Expand Macro 查看宏生成的代码。

报错处理

Declaration name 'isCat' is not covered by macro 'CaseDetection'

宏生成的代码非常完美,但是编辑报错了,这是因为宏生成出来的变量/方法需要在宏声明部分定义好,回到 MyMacro.swift 宏声明文件修改一下声明代码:

@attached(member, names: arbitrary)
public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro")

⚠️注意:arbitrary 表示宏可以生成任意变量/方法,在这个例子中,由于我们要生成的变量是动态变化的,所以只能写 arbitrary,如果你的宏生成的变量/方法是固定的,建议在这里也固定写死,比如:

@attached(member, names: named(isCat))
public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro")

我们再运行就发现编译通过了,最后的最后,记得去完善测试用例~

总结

宏非常强大,可以帮我们省去很多重复的代码,虽然写宏的过程会比较麻烦,但是写完之后就可以为你节省非常多的时间。另外每一个类型的宏都是 protocol,所以我们可以将多个宏组合在一起使用,比如 Swift Data 里面的 @Model。目前宏还在 beta 测试阶段,后续 Apple 也可能会对宏进行改进,我也会持续关注并更新哒。

本文由mdnice多平台发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 前言 通过阅读别人的优秀源码,你会发现别人的开源API设计中,有一些宏你是经常忽略的,或者你不知道的。通过这些宏,...
    gitKong阅读 5,149评论 5 41
  • 【转载】曾梦想仗剑走天涯 1.Xcode IDE概览 说明:从左到右,依次是“导航窗格(Navigator)->边...
    06a6a973d7ab阅读 3,826评论 2 20
  • XCode使用一:Xcode基本操作 传送至原文地址 1.Xcode IDE概览 说明:从左到右,依次是“导航窗格...
    无名小鱼会吐火阅读 28,804评论 0 23
  • 1.Xcode IDE概览 说明:从左到右,依次是“导航窗格(Navigator)->边列(Gutter)->焦点...
    小地阅读 5,357评论 0 9
  • 这个不错分享给大家,从扣上看到的,就转过来了 《电脑专业英语》 file [fail] n. 文件;v. 保存文...
    麦子先生R阅读 6,558评论 5 24