什么是宏
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ā)布