iOS Swift Macro入門

一、Swift Macro介紹

WWDC2023會(huì)上Swift 5.9加入了Swift Macro,它允許我們?cè)诰幾g時(shí)生成代碼或在編譯之前動(dòng)態(tài)地操作項(xiàng)目的 Swift 代碼,從而實(shí)現(xiàn)在編譯時(shí)注入額外的功能,使我們的應(yīng)用程序的代碼庫更易于閱讀且更高效地編碼。
Swift Macro沒有iOS版本要求,只要使用Swift 5.9及以上都可以。
OC Macro就是簡單的代碼替換,而 Swift 宏設(shè)計(jì)的比較抽象,可以通過框架提供的API直接獲取到Swift的句法樹,獲取或添加需要的代碼,功能更加強(qiáng)大。
Swift Macro源碼地址 注:本文基于510.0.3版本
本文主要目的是記錄以下Swift Macro的用法和每種類型的宏的作用,方便快速入門。如果內(nèi)容有誤或理解有誤,希望各位大佬指正,謝謝。

二、初探Swift Macro

1.創(chuàng)建Swift Macro工程

在Xcode的File菜單中選擇NEW->Package->Swift Macro,如圖:

Swift Macro

先看看默認(rèn)生成的Macro程序,它會(huì)默認(rèn)實(shí)現(xiàn)一個(gè)stringify Macro。
Macro文件結(jié)構(gòu)

2.文件結(jié)構(gòu)

這里只先簡單介紹一下,具體代碼實(shí)現(xiàn)及作用后文再詳細(xì)分析。

2.1.SwiftMacroMacro.swift 宏實(shí)現(xiàn)

struct StringifyMacro就是stringify macro實(shí)現(xiàn)的源碼。
structSwiftMacroPlugin類似于注冊(cè)宏,實(shí)現(xiàn)的宏都要添加到providingMacros數(shù)組里才會(huì)生效,否則使用時(shí)會(huì)報(bào)錯(cuò)。

Macro實(shí)現(xiàn)

2.2.SwiftMacro.swift 宏聲明

通過macro關(guān)鍵詞定義一個(gè)stringify宏,具體結(jié)構(gòu)和聲明方法類似。
等號(hào)右邊賦值部分又是另一個(gè)宏,這個(gè)宏專門用來定義其他宏的,傳入需定義宏所在模塊和類型(也就是上面的structStringifyMacro)。
@freestanding(expression)官方定義為MacroRole宏角色(如下圖MacroRole的定義)。相當(dāng)于定義這個(gè)宏具備什么功能,不同的關(guān)鍵詞決定了這個(gè)宏具備不同的功能,并且是可以添加多個(gè)這種關(guān)鍵詞的,具體每個(gè)角色等后文講解各類型宏作用時(shí)就自然理解了。

Macro 聲明

externalMacro
宏角色
2.3.main.swift 宏運(yùn)用

這個(gè)文件就是宏在項(xiàng)目中的實(shí)際運(yùn)用,可以看到傳入了a + b,返回了他們的相加結(jié)果a + b = 42,并且還能把"a + b"作為字符串返回。怎么做到的呢?等看完下文分析ExpressionMacro時(shí),自然就理解了。

宏運(yùn)用代碼

我們還可以右鍵宏,在菜單里點(diǎn)擊Expand Macro展開宏,也就是預(yù)覽這個(gè)宏編譯后會(huì)轉(zhuǎn)換成什么代碼,依次來確認(rèn)宏是否正確的生成我們想要的代碼。如果發(fā)現(xiàn)點(diǎn)擊后沒任何效果,就要排查宏是不是那里寫錯(cuò)了。
展開宏

預(yù)覽宏

2.3.SwiftMacroTests.swift 宏測(cè)試

除了上面直接使用宏來做測(cè)試以外,我們還可以用蘋果為我們提供的測(cè)試工具來進(jìn)行測(cè)試。這個(gè)測(cè)試類運(yùn)行后是可以在宏實(shí)現(xiàn)里面斷點(diǎn)調(diào)試的,2.2宏運(yùn)用運(yùn)行起來斷點(diǎn)是不起作用的。
等后面我們使用例子分析代碼時(shí),就會(huì)使用到這個(gè)測(cè)試類來斷點(diǎn)查看變量,追蹤問題。


測(cè)試

三、Swift Macro分類

Macro Protocol

宏歸類

從上圖可以看到官方為我們提供了很多種宏,但實(shí)際目前分為兩類宏。
1.FreestandingMacro:即獨(dú)立宏,宏聲明中使用 @freestanding關(guān)鍵字,宏運(yùn)用時(shí)以#開頭。獨(dú)立宏和OC的宏類似,就是將宏替換宏中預(yù)定義的代碼。 上一個(gè)章節(jié)中的#stringify(a + b)就是一個(gè)獨(dú)立宏。
2.AttachedMacro:附加宏,宏聲明中使用@attached關(guān)鍵字,宏運(yùn)用時(shí)以@開頭。作用是為類、結(jié)構(gòu)體、協(xié)議、屬性、方法等目標(biāo)聲明附加額外的代碼(只能增加,不能修改刪除原有代碼)
iOS17為我們提供的新的監(jiān)聽框架import Observation中的@Observable就是一個(gè)附加宏。
swift macro其實(shí)基礎(chǔ)入門并不難,比較麻煩的是需要處理各種語法、重復(fù)添加代碼等特殊情況,想要自己的宏更加完善,就需要兼容這些特殊情況。比如:

// 1.定義變量有多種語法,需要分別處理
var a: Int = 0 // 標(biāo)準(zhǔn)寫法,可以拿到變量類型
var a = 0 // 不能從語法樹中獲取變量類型,需要自己判斷
var a = 0, b = 0 // 有多個(gè)變量,需要遍歷獲取

// 2.給成員變量添加didSet訪問器,如果外部代碼已經(jīng)手動(dòng)添加了didSet。
//   宏內(nèi)就要自己做處理,根據(jù)自己的需求選擇拋異?;蛘叻祷乜?。

// 3.定義了一個(gè)給類使用的宏,不添加檢查代碼的話,外部是可以錯(cuò)誤的給協(xié)議、結(jié)構(gòu)體等添加的。

四、自定義宏

開始之前,有一些名詞需要先解釋一下:

  1. declaration聲明:指宏添加到的目標(biāo)對(duì)象,可以是類、結(jié)構(gòu)體、屬性、方法等。
  2. member成員:即屬性、方法。
  3. attribute屬性: 指聲明前面添加的修飾符,Swift Attributes。比如 @Observable、@available(*, deprecated)等,這些都算是屬性。

1.獨(dú)立宏

1.1.ExpressionMacro

在第二章節(jié)創(chuàng)建的宏工程默認(rèn)實(shí)現(xiàn)的宏StringifyMacro就是一個(gè)ExpressionMacro表達(dá)式宏。

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

        return "(\(argument), \(literal: argument.description))"
    }
}

在宏聲明中需要對(duì)表達(dá)式宏添加@freestanding(expression)來表示這個(gè)宏是一個(gè)表達(dá)式類型的獨(dú)立宏。

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

下圖就是此宏生成的Abstract Syntax Tree 抽象語法樹。可以很清晰的看到我們使用宏是傳入的表達(dá)式,多個(gè)操作符operator就會(huì)生成更深層級(jí)的嵌套。
此宏主要用于表達(dá)式的計(jì)算或轉(zhuǎn)換。需要傳入一個(gè)表達(dá)式,根據(jù)自身需求可對(duì)其中各節(jié)點(diǎn)進(jìn)行修改。
比如傳入的操作符+可以改成-,再將修改后的ExprSyntax作為返回參數(shù)傳遞出去,就能將原本的加法改為減法。
以下是宏#stringify(a + b + c)的語法樹結(jié)構(gòu):

抽象語法樹

1.2.DeclarationMacro

可以用來定義類、結(jié)構(gòu)體、枚舉、方法等,起到聲明的作用。如下代碼獲取外部傳入?yún)?shù).stringSegment(name) = expression.segments.first解析出參數(shù)字段name。然后生成了2個(gè)結(jié)構(gòu)體struct (name)struct (name)Sub

public struct DeclarationTestMacro: DeclarationMacro {
    public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        guard let expression = node.arguments.first?.expression.as(StringLiteralExprSyntax.self),
              expression.segments.count == 1,
              case let .stringSegment(name) = expression.segments.first else {
            return []
        }
        let syntax1 = DeclSyntax(stringLiteral:
            """
            struct \(name) { 
            var a = 0
            var b = 0
            }
            """)
        
        let syntax2 = DeclSyntax(stringLiteral:
            """
            struct \(name)Sub {
            var a = 0
            var b = 0
            }
            """)
        return [syntax1, syntax2]
    }
}

宏聲明如下:
因?yàn)檫@個(gè)宏定義了一個(gè)結(jié)構(gòu)體,這里需要標(biāo)記names,否則會(huì)報(bào)錯(cuò):
這個(gè)參數(shù)有4種定義:
1.arbitrary: 即任意名字都行
2.named(name): 必須是指定的名字
3.prefixed(name): 前綴必須是指定字符串(實(shí)際測(cè)試中疑似對(duì)declaration宏不生效,待確認(rèn))。
4.suffixed(name): 后綴必須是指定字符串(實(shí)際測(cè)試中疑似對(duì)declaration宏不生效,待確認(rèn))。

@freestanding(declaration, names: arbitrary)
public macro declarationStruct(_ value: String) = #externalMacro(module: "SwiftMacroMacros", type: "DeclarationTestMacro")

使用時(shí)展開宏即可預(yù)覽替換后的結(jié)果:


宏展開
1.3.CodeItemMacro

此宏還是一個(gè)未啟用的實(shí)驗(yàn)性功能,不能正式使用。但是可以在SwiftMacroTests測(cè)試類中調(diào)試。(由于是實(shí)驗(yàn)性質(zhì)的功能,這里就只粗略展示一下用法)

CodeItemMacro

在第二章節(jié)創(chuàng)建的宏工程的SwiftMacroMacro類實(shí)現(xiàn)中添加struct CodeItemTestMacro,實(shí)現(xiàn)CodeItemMacro協(xié)議。

CodeItemTestMacro宏只是簡單的實(shí)現(xiàn)了一個(gè)判斷傳入的參數(shù)是否等于0
比如將#codeItemTest(a, b)轉(zhuǎn)換為
if a == 0 {
return
}
if b == 0 {
return
}

具體的宏實(shí)現(xiàn)代碼如下(下文有講解具體代碼含義):

public struct CodeItemTestMacro: CodeItemMacro {
    public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.CodeBlockItemSyntax] {
        let identifiers = try node.arguments.map { argument in
          guard let declReferenceExpr = argument.expression.as(DeclReferenceExprSyntax.self) else {
              throw MacroError.codeItem
          }
            return declReferenceExpr.baseName
        }
        let items = identifiers.compactMap { syntax -> CodeBlockItemSyntax in
            """
            if \(raw: syntax.text) == 0 {
                return
            }
            """
        }
        return items
    }
}

enum MacroError: Error {
    case codeItem
}

測(cè)試用例中測(cè)試代碼如下:

    func testCodeItemMacro() throws {
        #if canImport(SwiftMacroMacros)
        assertMacroExpansion(
            """
            #codeItemTest(a, b)
            """,
            expandedSource: """
            if a == 0 {
                return
            }
            if b == 0 {
                return
            }
            """,
            macros: testMacros
        )
        #else
        throw XCTSkip("macros are only supported when running tests for the host platform")
        #endif
    }

//測(cè)試結(jié)果,可以看到測(cè)試通過:
Test Suite 'Selected tests' started at 2024-08-21 11:18:01.012.
Test Suite 'SwiftMacroTests.xctest' started at 2024-08-21 11:18:01.012.
Test Suite 'SwiftMacroTests' started at 2024-08-21 11:18:01.012.
Test Case '-[SwiftMacroTests.SwiftMacroTests testCodeItemMacro]' started.
Test Case '-[SwiftMacroTests.SwiftMacroTests testCodeItemMacro]' passed (0.008 seconds).
Test Suite 'SwiftMacroTests' passed at 2024-08-21 11:18:01.020.
     Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.008) seconds
Test Suite 'SwiftMacroTests.xctest' passed at 2024-08-21 11:18:01.021.
     Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.009) seconds
Test Suite 'Selected tests' passed at 2024-08-21 11:18:01.021.
     Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.009) seconds
Program ended with exit code: 0

打斷點(diǎn)追蹤協(xié)議方法都提供了哪些屬性可以讓我們獲取。從下圖可以看到node有個(gè)arguments數(shù)組,里面包含了傳入的參數(shù)信息,所以這個(gè)宏實(shí)現(xiàn)我們就是去獲取baseName,然后將baseName轉(zhuǎn)換為上面的if-else代碼。


斷點(diǎn)

node包含的數(shù)據(jù)

2.附加宏

2.1.AccessorMacro

這個(gè)宏是給成員變量附加訪問器的(比如didSet、willSet、getter、setter)。需要注意的是,如果本身已經(jīng)實(shí)現(xiàn)了didSet,宏又去添加一個(gè)didSet,不做處理的話會(huì)重復(fù)添加導(dǎo)致報(bào)錯(cuò),一般需要在宏實(shí)現(xiàn)中自己做判斷,下面的例子只是簡單的展示一下用法,不會(huì)做過多的容錯(cuò)處理。

public struct AccessorTestMacro: AccessorMacro {
    public static func expansion(of node: SwiftSyntax.AttributeSyntax,
                                 providingAccessorsOf declaration: some DeclSyntaxProtocol,
                                 in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
        guard let property = declaration.as(VariableDeclSyntax.self) else {
            return []
        }
        guard let pattern = property.bindings.first?.pattern.as(IdentifierPatternSyntax.self) else {
            return []
        }
        let identifier = pattern.identifier.trimmed
        if case .argumentList(let list) = property.attributes.first?.as(AttributeSyntax.self)?.arguments,
           let first = list.first,
           let statements = first.expression.as(ClosureExprSyntax.self)?.statements {
            let didSet: AccessorDeclSyntax =
              """
              didSet {
              print("宏內(nèi)部添加的代碼")
              \(statements.trimmed)
              }
              """
            return [didSet]
        } else {
            let didSet: AccessorDeclSyntax =
              """
              didSet {
              print(\(identifier))
              }
              """
            return [didSet]
        }
    }
}

宏聲明如下:
這里加了一個(gè)入?yún)lock,是為了演示外部可以通過一定的方法將要執(zhí)行的代碼塊添加到宏生成的訪問器里(這種實(shí)現(xiàn)有一定的局限性,比如不能隨意訪問self及其他成員變量),這里只做演示用。
@attached(accessor, names: named(didSet))由于AccessorMacro是一個(gè)附加宏,所以不能像上一節(jié)的獨(dú)立宏那樣使用freestanding來修飾了。然后指定只生成didSet訪問器。

/// 實(shí)驗(yàn)傳代碼塊
@attached(accessor, names: named(didSet))
public macro accessorTest<T>(_ block:((T)-> Void)? = nil) = #externalMacro(module: "SwiftMacroMacros", type: "AccessorTestMacro")
/// 常規(guī)用法
@attached(accessor, names: named(didSet))
public macro accessorTest() = #externalMacro(module: "SwiftMacroMacros", type: "AccessorTestMacro")

在下面的使用代碼中右鍵菜單展開宏可以預(yù)覽到最終代碼會(huì)是什么樣的。


宏運(yùn)用及結(jié)果

生成didSet訪問器很簡單,可以直接用字符串定義。然后是從下圖中打印的語法樹可以找到傳入的block,然后一步步提取其中的代碼塊,然后插入到didSet中(再次提醒此例子代碼比較簡單,許多容錯(cuò)都沒有處理)。
實(shí)際上@accessorTest<Int>({ a in print()}) 這里也是取巧并非真的從宏里面將a變量傳遞出來了,只是借用同名a讓代碼塊附加到didSet后能找到a這個(gè)變量。

declaration語法樹

2.2.ExtensionMacro

此宏用于創(chuàng)建Extension,下面的代碼創(chuàng)建了一個(gè)Extension并遵守Equatable,很簡單。

public struct ExtensionTestMacro: ExtensionMacro {
    public static func expansion(of node: AttributeSyntax, 
                                 attachedTo declaration: some DeclGroupSyntax,
                                 providingExtensionsOf type: some TypeSyntaxProtocol,
                                 conformingTo protocols: [TypeSyntax], 
                                 in context: some MacroExpansionContext
    ) throws -> [ExtensionDeclSyntax] {
        let syntax: ExtensionDeclSyntax? = 
        try? .init(.init(stringLiteral:
          """
          extension \(type.trimmed): Equatable {}
          """
        ))
        
        guard let syntax else {
            return []
        }
        return [syntax]
    }
}
@attached(extension, conformances: Equatable)
public macro extensionTest() = #externalMacro(module: "SwiftMacroMacros", type: "ExtensionTestMacro")
宏展開
2.3.MemberAttributeMacro

為聲明中的成員添加屬性。聲明、成員、屬性指什么,可以看文章第四節(jié)開頭。
這里我們就可以用此宏給聲明的成員附加我們上面2.1實(shí)現(xiàn)的accessorTest宏。不做判斷處理的話,默認(rèn)是會(huì)給所有成員附加屬性,我們也可以根據(jù)自己需要添加邏輯過濾不想附加屬性的成員。

public struct MemberAttributeTestMacro: MemberAttributeMacro {
    public static func expansion(of node: AttributeSyntax,
                                 attachedTo declaration: some DeclGroupSyntax,
                                 providingAttributesFor member: some DeclSyntaxProtocol,
                                 in context: some MacroExpansionContext
    ) throws -> [AttributeSyntax] {
        return [
            AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier("accessorTest"))),
        ]
    }
}
@attached(memberAttribute)
public macro memberAttributeTest() = #externalMacro(module: "SwiftMacroMacros", type: "MemberAttributeTestMacro")
宏展開
2.4.MemberMacro

為聲明添加額外的成員。
如下例子中:給聲明(即實(shí)現(xiàn)中為結(jié)構(gòu)體)添加了一個(gè)flag屬性(成員)和一個(gè)memberAttributePrint方法(成員)。該方法中打印了聲明的屬性和flag的值。

public struct MemberTestMacro: MemberMacro {
    public static func expansion(
      of node: AttributeSyntax,
      providingMembersOf declaration: some DeclGroupSyntax,
      conformingTo protocols: [TypeSyntax],
      in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        
        let syntax1: DeclSyntax =
        """
        var flag: Bool = false
        
        func memberAttributePrint() {
            print("\(node.attributeName.trimmed)-\\(flag)")
        }
        """
        return [syntax1]
    }
}
@attached(member, names: arbitrary)
public macro memberTest() = #externalMacro(module: "SwiftMacroMacros", type: "MemberTestMacro")
宏展開
2.5.PeerMacro

根據(jù)成員附加一個(gè)新成員。
下面的例子中,我們根據(jù)屬性生成了一個(gè)新的屬性var (a)Peer: Int = 0,新的屬性的類型、初始化與目標(biāo)屬性保持一致,并且還附帶了@accessorTest宏,為新屬性添加didSet訪問器。

public struct PeerTestMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let property = declaration.as(VariableDeclSyntax.self) else {
            return []
        }
        guard let identifier = property.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.trimmed,
              let type = property.bindings.first?.typeAnnotation?.type.trimmed,
              let initializer = property.bindings.first?.initializer else {
            return []
        }
        
        let syntax: DeclSyntax =
        """
        @accessorTest
        var \(identifier)Peer: \(type) \(raw: initializer)
        """
        
        return [syntax]
    }
}
@attached(peer, names: arbitrary)
public macro peerTest() = #externalMacro(module: "SwiftMacroMacros", type: "PeerTestMacro")

下面是宏展開的效果:
這里出Bug了,@accessorTest宏無法展開,但通過運(yùn)行可以看出附加的訪問器是生效了的,打印了屬性的值5


宏展開
2.6.聚合使用

我們可以一個(gè)宏實(shí)現(xiàn)里實(shí)現(xiàn)多個(gè)宏協(xié)議,然后聲明宏時(shí)使用多個(gè)@attached修飾。

@attached(accessor, names: named(didSet))
@attached(peer, names: arbitrary)
public macro ObservableTracked() = #externalMacro(module: "ObservationMacros", type: "ObservableTrackedMacro")

五、框架中常見的類(待補(bǔ)充)

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 學(xué)習(xí)目標(biāo) 由于已經(jīng)有了Java編程思想,所以著重了解Swift語言特有的特性,與Java不一樣的地方。最終目的是可...
    Zhouztashin閱讀 1,290評(píng)論 0 2
  • Hello Word 在屏幕上打印“Hello, world”,可以用一行代碼實(shí)現(xiàn): 你不需要為了輸入輸出或者字符...
    restkuan閱讀 3,365評(píng)論 0 6
  • 本文寫于2014年6月,內(nèi)容短小精湛,通過簡單的例子,把Swift語言中幾個(gè)主要的點(diǎn)展現(xiàn)出來,并配合一些簡單的小練...
    Frederic曉代碼閱讀 2,203評(píng)論 0 1
  • # 屬性 (Properties)本頁包含內(nèi)容: - 存儲(chǔ)屬性(Stored Properties) - 計(jì)算屬性...
    刺骨寒閱讀 498評(píng)論 1 0
  • 什么是宏 Apple 在 Swift 5.9 里面加入了 Swift macros(宏),宏可以在編譯的過程中幫我...
    RayJiang97閱讀 1,373評(píng)論 0 3

友情鏈接更多精彩內(nèi)容