聊聊Swift中的宏

聊聊Swift中的宏

宏,Macros是一種常見的編程技術,傳統的C語言中,即包含了宏功能。宏這種功能,簡單來說是在代碼的預編譯階段進行靜態替換,是一種非運行時的特性。但是往復雜了說,宏實際上也提供了一種”元編程“方式,即對程序本身進行編程。如果真正掌握宏的應用,又比較複雜,以C語言中的宏爲例,宏可以有參數,可以進行嵌套展開,要編寫質量高的宏,還是非常有難度。這裏附上之前的一篇關於Objective-C下宏的應用博文,以供需要的朋友參考:

https://my.oschina.net/u/2340880/blog/3357392

Swift宏簡介

最初的Swift版本其實並不支持宏,這其實也和Swift語言的設計理念有關,C語言中的宏應用廣泛,但是編譯時展開的特性會是代碼的可讀性下降,也會增加代碼的漏洞風險。Swift秉承安全、易理解、易使用的設計初衷,並沒有引入宏的概念。但宏的元編程能力可以大大的提高編程的靈活性和複用性,Swift在5.9版本中重新引入了宏功能,並且是以一種全新的方式來定義和實現宏,在提供靈活性的同時保證代碼的安全性和可靠性。但這也有一些缺陷,相比與C語言的宏,Swift中的宏的定義非常抽象,實現複雜,不太利於開發者進行理解。本篇文章即基於這一前提,希望可以系統簡介的對Swift中的宏進行介紹,幫助更多開發者瞭解它,使用它。

首先,在做詳細介紹前,我們需要先牢記幾個核心原理:

1 - 宏會在編譯代碼前進行代碼轉換,即預編譯階段進行處理。

2 - 宏在展開時,永遠只會增加代碼,不會修改或刪除原始的代碼。(重點)

3 - 宏的輸入和輸出都會經過編譯器的檢查,保證其語法正確,並且如果宏展開後的實現發現異常,也會被處理爲編譯時異常。

上述的原理1和原理3無需特別關注,只需要知道宏是一個編譯時的特性即可,原理2是非常重要的,當我們想將某個功能點編寫爲宏時,首先要考慮的是我們是要附加功能還是刪改功能,如果是增加功能則非常適合使用宏,如果是刪改邏輯則應該早早放棄,宏永遠不應該刪改原本的代碼。

Swift中的宏分爲兩類:

1 - 獨立宏

2 - 附加宏

其中,獨立宏單獨出現,單獨使用,不會附加到任何聲明(可以理解爲原始代碼)上。附加宏則需要配合聲明一起使用,通常是爲了向原代碼中增加一些功能。從特性上看,獨立宏與C語言的宏有些類似,做簡單的代碼展開或靜態替換很方便。附加宏則更像是一種裝飾器模式的應用,爲原始邏輯進行包裝,附加功能。這兩種宏從聲明到用法上都有區別。

獨立宏

獨立宏使用"#"來調用,因此當你在代碼中看到#相關的語法時,就要意識到這是一個宏,且是一個獨立宏。標準庫中默認提供了一些獨立宏可以直接使用,例如:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print(#file, #function, #line)
        #warning("系統宏,顯示警告信息")
    }
}

上面代碼中,#file,#function,#line和#warning都是獨立宏,前3個宏無參數,在編譯時分別替換爲當前文件名、當前函數名和當前行號,#warning宏有參數,用來爲告訴編譯器這裏展示一條警告信息。這些宏因爲是標準庫中的,我們無法查看展開後的樣子,如果是自定義宏則可以直接展開查看,後面我們再介紹。

附加宏

使用”@“來調用附加宏,附加宏用來補充其所聲明的代碼,爲原始代碼添加新的功能,附加宏比較複雜,後面我們再詳細介紹。

宏的聲明、定義與實現

Swift語言和C語言的一大區別在於Swift一般無需做聲明,如函數、變量、類等,直接定義即可使用。但宏卻不同,宏必須進行聲明,聲明的主要作用是指定宏的名稱、參數以及類型和使用場景。

與普通的Swift功能代碼不同,每個宏都是一個單獨的Swift包,在工程中我們可以創建一個新的Package,選擇Swift Macro,如下圖所示:

宏的實現依賴於swift-syntax包,Xcode會自動幫我們加載好依賴。創建好的的Package會自動生成模版文件,我們只需要關係Sources和Tests文件夾下的內容即可。自動生成的模板中的宏是使用了swift-syntax包的Swift源代碼靜態分析能力,略爲複雜,增加了理解宏本身的難度。這裏我們可以不理會這部分,專注於宏本身的邏輯。

首先,一個宏模塊分爲聲明,實現,測試和使用4個部分。下面我們逐一來進行介紹。

宏的聲明

獨立宏聲明

獨立宏使用@freestanding來進行聲明,在聲明宏時,需要指定宏的角色。獨立宏有兩種角色:

expression:創建一段有返回值的代碼。

declaration:聲明類宏,用來創建聲明類的代碼。

例如我們聲明一個角色爲expression的宏,如下:

@freestanding(expression)
public macro AppendHello(_ msg: String) -> String = #externalMacro(module: "MyMacroMacros", type: "AppendHelloMacro")

代碼中,@freestanding(expression)指定了當前宏是一個表達式角色的獨立宏,#externalMacro是Swift內置的一個宏,指定了當前宏所對應的模塊名以及類型標識。

聲明一個declaration角色的宏如下:

@freestanding(declaration, names: arbitrary)
public macro MakeStatic(_ name: String) = #externalMacro(module: "MyMacroMacros", type: "MakeStaticMacro")

需要注意,宏在指定角色時,可以通過names參數來對要使用的符號進行定義,以上面的宏聲明爲例,MakeStatic的作用是會生成一個靜態變量,因此會在原代碼中新增符號,但是變量的名稱是由參數決定的,因此需要將names參數設置爲arbitrary,表示要生成的符號是不定的。

names參數可填爲:

1 named(xxx) 具體的符號名稱。

2 overloaded 對原符號的重載

3 prefixed(xxx) 增加前綴

4 suffixed(xxx) 增加後綴

5 arbitrary 動態符號名稱

附加宏聲明

附加宏使用@attached來進行聲明,與獨立宏類似,其也需要指定角色:

peer:對等角色,與所附加的原代碼在相同的層級上增加代碼,例如增加函數的重載。

member:成員角色,爲所附加的原代碼增加內部成員,如增加屬性等。

memberAttribute:成員屬性角色,爲所附加的源代碼的內部成員增加屬性。

accessor:訪問器角色,爲所附加的源代碼增加Getter,Setter方法等。

extension(之前爲conformance,最新swift版本修改爲extension):遵守着角色,爲所附加的源代碼增加協議和約束。

我們先來定義一個peer角色類型的宏,用來實現一個自動生成的重載函數,此重載函數會增強原函數的功能,添加函數的執行時間日誌。如下:

@attached(peer, names: overloaded)
public macro OverrideForAPM() = #externalMacro(module: "MyMacroMacros", type: "OverrideForAPMMacro")

這裏我們將names指定爲了overloaded,表示對原符號的重載操作。後面會有此宏的實現示例。

member角色的宏通常用來爲類或結構增加成員變量或方法等,聲明示例如下:

@attached(member, names: named(logSelf))
public macro MemberLog() = #externalMacro(module: "MyMacroMacros", type: "MemberLogMacro")

其中,我們聲明時明確定義了要引入的符號logSelf,此符號將作爲生成的函數名。

memberAttribute角色宏本質上是作用於類或結構的成員上,用來爲成員增加修飾,例如可以定義一個宏爲類的成員都默認加上@objc修飾:

@attached(memberAttribute)
public macro Objc() = #externalMacro(module: "MyMacroMacros", type: "ObjcMacro")

accessor角色宏用在具體的成員上,用來增加訪問器邏輯,例如下面的聲明,此宏將爲訪問器自動生成計算屬性邏輯:

@attached(accessor)
public macro GetLog() = #externalMacro(module: "MyMacroMacros", type: "GetLogMacro")

extension宏用來爲原結構增加一些協議或遵守一些規則,例如我們可以定義一個宏,來讓所修飾的修改自動實現Equatable協議:

@attached(extension, conformances:Equatable ,names: named(==))
public macro EqualProtocol() = #externalMacro(module: "MyMacroMacros", type: "EqualProtocolMacro")

其中conformances參數指定要遵守的協議,因爲我們同時要對協議進行實現,會引入新的符號,因此需要names參數中也指明。

宏的實現

宏的實現,即也是宏的定義。指定了宏具體要實現的邏輯。

獨立宏實現

根據前面AppendHello宏的聲明,在MyMacroMacros可以對其進行實現,代碼如下:

public struct AppendHelloMacro: ExpressionMacro {
    public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
        // 解析宏的參數,此宏我們定義了一個msg參數
        guard let argument = node.argumentList.first?.expression, let segment = argument.as(StringLiteralExprSyntax.self)?.segments.first else {
            fatalError("編譯異常")
        }
        // 我們需要將靜態代碼內部的字符串數據解析出來
        switch segment {
        case .stringSegment(let string):
            // 返回一個靜態字符串表達式
            return ExprSyntax(stringLiteral: "\"\(string.content.text + "Hello")\"")
        default:
            fatalError("編譯異常")
        }
    }
}

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


所有的表達式角色的獨立宏,在定義時需要實現ExpressionMacro協議,此協議中的expansion函數將返回展開後的結果,我們可以根據邏輯來返回數據即可。需要注意,在編寫宏時,我們所有做的操作都是元編程操作,因此需要對Swift元代碼進行解析與處理,這也是swift-syntax主要提供的功能。代碼中的解析邏輯你可以暫時無需關注。另外,在Plugin的定義中,我們要將此宏類實例進行返回,這裏的類與我們前面聲明時的類標識要一致。

MakeStatic宏的定義方法也類似,只是其需要實現DeclarationMacro協議,角色爲聲明類型的宏主要是爲原代碼增加一些聲明,如增加屬性,增加方法,增加協議等等。爲了演示方便,MakeStatic的作用是根據傳入的字符串生成一個靜態變量,如下:

public struct MakeStaticMacro: DeclarationMacro {
    public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        // 解析宏的參數,此宏我們定義了一個name參數
        guard let argument = node.argumentList.first?.expression, let segment = argument.as(StringLiteralExprSyntax.self)?.segments.first else {
            fatalError("編譯異常")
        }
        // 我們需要將靜態代碼內部的字符串數據解析出來
        switch segment {
        case .stringSegment(let string):
            // 返回一個靜態字符串表達式
            return ["static var \(string): Any?"]
        default:
            fatalError("編譯異常")
        }
    }
}


附加宏實現

OverrideForAPM宏的實現如下:

public struct OverrideForAPMMacro: PeerMacro {
    public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] {
        // 函數的聲明部分
        guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
            fatalError("編譯異常")
        }
        // 函數的實現副本
        if let body = functionDecl.body {
           return [
           """
           func \(functionDecl.name)(_ apm: Bool)  {
           if apm {print(Date())}
           \(body.statements)
           if apm {print(Date())}
           }
           """
           ]
        }
        return []
    }
}

MemberLog宏的實現如下:

public struct MemberLogMacro: MemberMacro {
    public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        return [
        """
        func logSelf(){
        print(self)
        }
        """
        ]
    }
}

Objc宏的實現如下:

public struct ObjcMacro: MemberAttributeMacro {
    public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingAttributesFor member: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.AttributeSyntax] {
        return ["@objc"]
    }
}

GetLog宏的實現如下:

public struct GetLogMacro: AccessorMacro {
    public static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
        // 獲取所修飾的符號
        guard
             let property = declaration.as(VariableDeclSyntax.self),
             let binding = property.bindings.first,
             let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed
           else {
             return []
           }
         return [
         """
         get {
            print("獲取計算屬性值", _\(identifier))
            return _\(identifier)
         }
         """,
         """
         set {
            _\(identifier) = newValue
         }
         """
         ]
    }
}

EqualProtocol 宏的實現如下:

public struct EqualProtocolMacro: 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] {
        return [try ExtensionDeclSyntax("extension \(type.trimmed): Equatable { static func == (lhs: \(type.trimmed), rhs: \(type.trimmed)) -> Bool { lhs == rhs}}")]
    }
}

對於附加宏來說,除了上述示例的場景外,我們也可以對某個宏指定多個角色,例如member角色宏和accessor角色宏,可以同時爲所修飾的原結構增加內部屬性和外部訪問器方法。多個角色宏的實現也類似,只需要具體的實現多個協議即可了。

宏的使用

宏的使用非常簡單,創建的宏Package中自動生成了一個main.swift文件,我們可以在其中進行使用測試,例如:

使用獨立的表達式宏:

// newString將被賦值爲 Xiao mingHello
let newString = #AppendHello("Xiao ming ")
print(newString)


使用獨立的聲明宏:

class MySingle {
    // 會被展開爲 static var obj: Any?
    #MakeStatic("obj")
}


使用peer宏:

// 此宏編譯後會增加一個新的重載函數,如下:
//func myFunc(_ apm: Bool)  {
//    if apm {
//        print(Date())
//    }
//
//        print("MyFuncCall")
//    if apm {
//        print(Date())
//    }
//}
@OverrideForAPM
func myFunc() {
    print("MyFuncCall")
}
// 調用將打印:
//2024-04-17 14:44:29 +0000
//MyFuncCall
//2024-04-17 14:44:29 +0000
myFunc(true)

使用member宏:

@MemberLog
class CustomClass {
    // 將像類中添加方法:
    // func logSelf() {
    //    print(self)
    //}
}

let c = CustomClass()
// MyMacroClient.CustomClass
c.logSelf()

使用memberAttribute宏:

@Objc
class SwiftObjcClass {
    // 宏展開有會增加 @objc
    func func1() {}
    // 宏展開有會增加 @objc
    var v1 = 0
    // 宏展開有會增加 @objc
    static let s1 = 0
}


使用accessor宏:

class GetLogDemo {
    var _prop = 0
    @GetLog
    var prop:Int
    // 下面將被展開
    //    {
    //        get {
    //           _prop
    //        }
    //        set {
    //           _prop = newValue
    //        }
    //    }
}

let d = GetLogDemo()
d.prop = 2
// 獲取計算屬性值 2
print(d.prop)

使用extension宏:

@EqualProtocol
class MyNumber {
    
}
// 會在下面展開
//extension MyNumber: Equatable {
//    static func == (lhs: MyNumber, rhs: MyNumber) -> Bool {
//        lhs == rhs
//    }
//}

宏的調試與測試

可以發現,宏的代碼編寫思路與常規的應用開發思路有很大不同,我們主要需要處理的是對Swift代碼本身的語法樹結構的解析與補充。當然,大部分工作swift-syntax包都幫我們處理好了。在開發宏時,我們可以直接在使用處右鍵將宏進行展開,可以直接看到宏編譯後的結果,例如:

如果宏展開後的結果比較複雜,我們也可以在運行時進行斷點,將宏展開,然後直接進行斷點調試即可。

另外,如果想要對宏本身進行斷點調試,則我們需要通過單元測試來運行宏,模板代碼中已經默認生成了測試代碼,例如對AppendHello宏進行單測,修改測試文件如下:

import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest

// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests.
#if canImport(MyMacroMacros)
import MyMacroMacros

let testMacros: [String: Macro.Type] = [
    "AppendHello": AppendHelloMacro.self,
]
#endif

final class MyMacroTests: XCTestCase {
    func testMacro() throws {
        #if canImport(MyMacroMacros)
        assertMacroExpansion(
            """
            #AppendHello("Xiao Li ")
            """,
            expandedSource: """
            "Xiao Li Hello"
            """,
            macros: testMacros
        )
        #else
        throw XCTSkip("macros are only supported when running tests for the host platform")
        #endif
    }
}

單測的邏輯也比較簡單,即我們給一個輸入宏,然後與預期的展開結果進行對比即可,因爲宏是靜態展開,因此非常容易也很適合進行單測。在單測執行時,我們是可以對宏的實現部分進行斷點的,通過斷點,可以對其輸入參數的詳細信息進行查看,方便我們宏邏輯的編寫,以上述單測爲例,斷點可以後可查看語法節點數據,如下:

(lldb) po node
MacroExpansionExprSyntax
├─pound: pound
├─macroName: identifier("AppendHello")
├─leftParen: leftParen
├─arguments: LabeledExprListSyntax
│ ╰─[0]: LabeledExprSyntax
│   ╰─expression: StringLiteralExprSyntax
│     ├─openingQuote: stringQuote
│     ├─segments: StringLiteralSegmentListSyntax
│     │ ╰─[0]: StringSegmentSyntax
│     │   ╰─content: stringSegment("Xiao Li ")
│     ╰─closingQuote: stringQuote
├─rightParen: rightParen
╰─additionalTrailingClosures: MultipleTrailingClosureElementListSyntax

寫在最後

本篇文章,單純從Swift宏的角度介紹了各種宏的使用方法和應用場景,然而真正要寫好宏,其實還是比較有難度的,首先在編寫時展開結果並不直觀,其次要考慮的邊界情況也很多,因此單測試一個非常好的保證質量的工具。另外,能夠熟練使用swift-syntax包也是寫好宏的基礎。有時間,後面在專門整理swift-syntax的用法吧,希望本篇文章可以爲你帶來一些幫助和啓發,感謝你使用寶貴時間閱讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章