開始寫一個(gè) Swift 宏吧

什么是宏

Apple 在 Swift 5.9 里面加入了 Swift macros(宏)踊东,宏可以在編譯的過程中幫我們生成一些需要重復(fù)編寫的代碼。WWDC 23 中有兩個(gè)關(guān)于宏的 Session胞四,Expand on Swift macros 介紹了什么是宏和宏的幾種類型恬汁,Write Swift macros 介紹了怎么去寫一個(gè)宏。這兩個(gè) Session 介紹了每種宏可以做什么辜伟,但是缺少了詳細(xì)的代碼氓侧,我不知道具體要怎么去實(shí)現(xiàn)我想要的效果,在查閱了一些資料Swift 官方庫的內(nèi)部實(shí)現(xiàn)之后才知道每個(gè)宏的定義和用法导狡。

宏類型介紹

宏主要分為兩種類型:

@freestanding 是一個(gè)獨(dú)立的宏(與 # 語法一起使用)约巷,并且可以用作表達(dá)式。

@attached 是一個(gè)附加宏(與 @ 語法一起使用)旱捧,需要搭配 struct/class/enum/property/function 等類型使用独郎,可以為其添加代碼踩麦。

每個(gè)類型的宏具體能干什么?

@freestanding(expression)

編寫一段代碼使其返回一個(gè)值氓癌。

let url = #URL("https://www.baidu.com")
// 宏內(nèi)部會判斷該字符串能否生成 URL谓谦,如果無法生成會報(bào)錯,將運(yùn)行報(bào)錯提前到了編譯階段贪婉。
let url = #URL("https:// www.baidu.com") // 報(bào)錯:UnableToCreateURL

宏生成代碼:

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

<details>
<summary>宏的實(shí)現(xiàn)代碼(點(diǎn)擊查看)</summary>

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

/// 實(shí)現(xiàn)
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反粥,報(bào)錯
        }
        return "URL(string: \"\(raw: content)\")!"
    }
}

</details>

@freestanding(declaration)

宏可以寫在任意地方,可以創(chuàng)建一段或多段代碼疲迂。

#guardValue(self)

宏生成代碼:

guard let self = self else { return }

<details>
<summary>宏的實(shí)現(xiàn)代碼(點(diǎn)擊查看)</summary>

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

/// 實(shí)現(xiàn)
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)

宏會在同個(gè)代碼層級生成一段代碼才顿。

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

宏生成代碼:

// 宏會在同個(gè)代碼層級生成代碼
func fetchDetail(_ id: Int, completionHandler: @escaping (String?) -> Void) {
  Task {
    completionHandler(await fetchDetail(id))
  }
}

<details>
<summary>宏的實(shí)現(xiàn)代碼(點(diǎn)擊查看)</summary>

該宏來自 Swift 官方庫
聲明
實(shí)現(xiàn)

</details>

目前在 beta 1 中生成出來的代碼無法直接被調(diào)用,不清楚是否是宏寫的有問題鬼譬,還是有 Bug娜膘。我更傾向這是 Bug逊脯,上面提到的 #guardValue 宏也無法調(diào)用到解包后的變量优质。如果是我用法的問題,麻煩在評論區(qū)告訴我军洼。

@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>宏的實(shí)現(xiàn)代碼(點(diǎn)擊查看)</summary>

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

/// 實(shí)現(xiàn)
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>宏的實(shí)現(xiàn)代碼(點(diǎn)擊查看)</summary>

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

/// 實(shí)現(xiàn)
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 }
    }
}

宏的實(shí)現(xiàn)代碼在后面的案例中。

@attached(conformance)

可以給 struct/class 添加協(xié)議和約束德谅。

@TestConformance
struct Foo { }

宏生成代碼:

extension Foo : SomeProtocol where AAA: BBB {}

<details>
<summary>宏的實(shí)現(xiàn)代碼(點(diǎn)擊查看)</summary>

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

/// 實(shí)現(xiàn)
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>

怎么自己創(chuàng)建宏

寫宏的準(zhǔn)備工作

1.創(chuàng)建工程

新建一個(gè) Swift Macro Package 爹橱,Xcode -> File -> New -> Package,選擇 Swift Macro窄做。

Swift Macro 需要依賴 apple/swift-syntax 第三方庫愧驱,這是 Apple 的詞法分析庫,用于解析椭盏、檢查组砚、生成和轉(zhuǎn)換 Swift 源代碼。

創(chuàng)建完成后掏颊,我們可以看到項(xiàng)目的結(jié)構(gòu)是這樣的:

├── Package.resolved
├── Package.swift
├── Sources
│   ├── MyMacro
│   │   └── MyMacro.swift // 宏聲明文件
│   ├── MyMacroClient
│   │   └── main.swift // 可運(yùn)行文件惫确,可以在這里測試宏的實(shí)際效果
│   └── MyMacroMacros
│       └── MyMacroMacro.swift // 宏實(shí)現(xiàn)文件
└── Tests
    └── MyMacroTests
        └── MyMacroTests.swift // 宏測試文件,用于編寫、調(diào)試宏
2.宏實(shí)現(xiàn)文件

我們先打開 MyMacroMacro.swift 寫一下上面提到的 @CaseDetection 宏改化。先讓宏遵守 MemberMacro 協(xié)議掩蛤,然后點(diǎn)擊報(bào)錯讓 Xcode 生成協(xié)議方法,生成之后先返回一個(gè)空數(shù)據(jù)陈肛,并將斷點(diǎn)打到 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 文件聲明一下宏:

// 如果宏遵守了多個(gè)協(xié)議阳藻,需要在這里寫上多個(gè) @attched()
@attached(member)
public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro")
4.宏測試文件

打開 MyMacroTests.swift 文件寫一個(gè)測試用例,目的是為了能斷點(diǎn)打到宏里面谈撒。

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

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

再寫一個(gè)測試用例腥泥,這里 expandedSource 是宏預(yù)期生成出來的代碼,我們可以先不寫啃匿。

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

運(yùn)行測試用例蛔外,我們就會進(jìn)入宏實(shí)現(xiàn)的斷點(diǎn)里面了,這時(shí)候我們可以開始寫宏了溯乒。

開始寫宏

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

node 參數(shù)可以獲取宏的聲明部分夹厌,如果宏接收參數(shù)可以從 node 中取到,執(zhí)行 po node裆悄。

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

如果我們想要獲取宏的名稱可以這樣寫:

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

declaration 參數(shù)可以獲取類型里面的定義矛纹,執(zhí)行 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
調(diào)試

宏需要獲取枚舉的名稱,我們現(xiàn)在斷點(diǎn)里面獲取到想要的數(shù)據(jù)光稼,再去寫代碼或南。

我們一步步去點(diǎn)開,會發(fā)現(xiàn)到 decl 就下不去了艾君。

po declaration.memberBlock.members.first!.decl

因?yàn)?decl 是頂層的協(xié)議 DeclSyntax采够,我們需要使用 as() 將其轉(zhuǎn)換為 EnumCaseDeclSyntax

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

在寫宏的過程中,我們會經(jīng)常遇到這個(gè)問題腻贰,發(fā)現(xiàn)類型對不上可以用 as() 進(jìn)行類型轉(zhuǎn)換吁恍,最終的調(diào)試代碼:

po declaration.memberBlock.members.first!.decl.as(EnumCaseDeclSyntax.self)?.elements.first!.identifier.description // "cat"
宏實(shí)現(xiàn)代碼

根據(jù)這個(gè)調(diào)試代碼,我們可以去寫宏實(shí)現(xiàn)代碼了播演。

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 { // 循環(huán)獲取所有屬性冀瓦、方法
            let elements = member.decl.as(EnumCaseDeclSyntax.self)?.elements
            if let propertyName = elements?.first?.identifier.description {
                names.append(propertyName) // 取出枚舉名
            }
        }
        
        return names.map { // 拼接實(shí)現(xiàn)代碼
            """
            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 里面寫一個(gè)枚舉測試一下宏。

@CaseDetection
enum Animal {
    case cat
}

寫完我們可以右擊 @CaseDetection 宏写烤,點(diǎn)擊 Expand Macro 查看宏生成的代碼翼闽。

報(bào)錯處理

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

宏生成的代碼非常完美,但是編輯報(bào)錯了洲炊,這是因?yàn)楹晟沙鰜淼淖兞?方法需要在宏聲明部分定義好感局,回到 MyMacro.swift 宏聲明文件修改一下聲明代碼:

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

??注意:arbitrary 表示宏可以生成任意變量/方法尼啡,在這個(gè)例子中,由于我們要生成的變量是動態(tài)變化的询微,所以只能寫 arbitrary崖瞭,如果你的宏生成的變量/方法是固定的,建議在這里也固定寫死撑毛,比如:

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

我們再運(yùn)行就發(fā)現(xiàn)編譯通過了书聚,最后的最后,記得去完善測試用例~

總結(jié)

宏非常強(qiáng)大藻雌,可以幫我們省去很多重復(fù)的代碼雌续,雖然寫宏的過程會比較麻煩,但是寫完之后就可以為你節(jié)省非常多的時(shí)間胯杭。另外每一個(gè)類型的宏都是 protocol驯杜,所以我們可以將多個(gè)宏組合在一起使用,比如 Swift Data 里面的 @Model做个。目前宏還在 beta 測試階段鸽心,后續(xù) Apple 也可能會對宏進(jìn)行改進(jìn),我也會持續(xù)關(guān)注并更新噠叁温。

本文由mdnice多平臺發(fā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末再悼,一起剝皮案震驚了整個(gè)濱河市核畴,隨后出現(xiàn)的幾起案子膝但,更是在濱河造成了極大的恐慌,老刑警劉巖谤草,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跟束,死亡現(xiàn)場離奇詭異,居然都是意外死亡丑孩,警方通過查閱死者的電腦和手機(jī)冀宴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來温学,“玉大人略贮,你說我怎么就攤上這事≌提” “怎么了逃延?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長轧拄。 經(jīng)常有香客問我揽祥,道長,這世上最難降的妖魔是什么檩电? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任拄丰,我火速辦了婚禮府树,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘料按。我一直安慰自己奄侠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布载矿。 她就那樣靜靜地躺著遭铺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪恢准。 梳的紋絲不亂的頭發(fā)上魂挂,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機(jī)與錄音馁筐,去河邊找鬼涂召。 笑死,一個(gè)胖子當(dāng)著我的面吹牛敏沉,可吹牛的內(nèi)容都是我干的果正。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盟迟,長吁一口氣:“原來是場噩夢啊……” “哼秋泳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起攒菠,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤迫皱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后辖众,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卓起,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年凹炸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了戏阅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡啤它,死狀恐怖奕筐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情变骡,我是刑警寧澤离赫,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站锣光,受9級特大地震影響笆怠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜誊爹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一蹬刷、第九天 我趴在偏房一處隱蔽的房頂上張望瓢捉。 院中可真熱鬧,春花似錦办成、人聲如沸泡态。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽某弦。三九已至,卻和暖如春而克,著一層夾襖步出監(jiān)牢的瞬間靶壮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工员萍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腾降,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓碎绎,卻偏偏與公主長得像螃壤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子筋帖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 前言 通過閱讀別人的優(yōu)秀源碼奸晴,你會發(fā)現(xiàn)別人的開源API設(shè)計(jì)中,有一些宏你是經(jīng)常忽略的日麸,或者你不知道的寄啼。通過這些宏,...
    gitKong閱讀 5,161評論 5 41
  • 【轉(zhuǎn)載】曾夢想仗劍走天涯 1.Xcode IDE概覽 說明:從左到右赘淮,依次是“導(dǎo)航窗格(Navigator)->邊...
    06a6a973d7ab閱讀 3,830評論 2 20
  • XCode使用一:Xcode基本操作 傳送至原文地址 1.Xcode IDE概覽 說明:從左到右辕录,依次是“導(dǎo)航窗格...
    無名小魚會吐火閱讀 28,991評論 0 23
  • 1.Xcode IDE概覽 說明:從左到右睦霎,依次是“導(dǎo)航窗格(Navigator)->邊列(Gutter)->焦點(diǎn)...
    小地閱讀 5,363評論 0 9
  • 這個(gè)不錯分享給大家梢卸,從扣上看到的,就轉(zhuǎn)過來了 《電腦專業(yè)英語》 file [fail] n. 文件副女;v. 保存文...
    麥子先生R閱讀 6,564評論 5 24