マクロとは何ですか
Apple は、Swift 5.9 に Swift マクロ (マクロ) を追加しました。マクロは、コンパイル プロセス中に繰り返し記述する必要があるコードを生成するのに役立ちます。WWDC 23 にはマクロに関するセッションが 2 つあります。「Swift マクロの拡張」では、マクロとは何かといくつかの種類のマクロを紹介します。「Swift マクロの作成」では、マクロの作成方法を紹介します。これらの 2 つのセッションでは、各マクロができることを紹介していますが、詳細なコードがないため、希望する効果を実現する方法がわかりません。いくつかの情報と Swift の公式ライブラリの内部実装を調べた後、それぞれの定義と使用法がわかります。マクロの。
マクロタイプの概要
マクロには主に 2 つのタイプがあります。
@freestanding
はスタンドアロン マクロ (#
構文) であり、式として使用できます。
@attached
これは追加のマクロ ( @
syntax、 struct/class/enum/property/function などの型で使用する必要があり、コードを追加できます。
それぞれの種類のマクロで具体的に何ができるのでしょうか?
@freestand(式)
値を返すコードを作成します。
let url = #URL("https://www.baidu.com")
// 宏内部会判断该字符串能否生成 URL,如果无法生成会报错,将运行报错提前到了编译阶段。
let url = #URL("https:// www.baidu.com") // 报错:UnableToCreateURL
マクロで生成されたコード:
let url = URL(string: "https://www.baidu.com")!
マクロ実装コード (クリックして表示)
/// 声明
@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)\")!"
}
}
@freestand(宣言)
マクロはどこにでも記述でき、1 つ以上のコードを作成できます。
#guardValue(self)
マクロで生成されたコード:
guard let self = self else { return }
マクロ実装コード (クリックして表示)
/// 声明
@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 }"
]
}
}
@attached(ピア)
マクロは同じコード レベルでコードを生成します。
@AddCompletionHandler()
func fetchDetail(_ id: Int) async -> String? { }
マクロで生成されたコード:
// 宏会在同个代码层级生成代码
func fetchDetail(_ id: Int, completionHandler: @escaping (String?) -> Void) {
Task {
completionHandler(await fetchDetail(id))
}
}
マクロ実装コード (クリックして表示)
このマクロは、Swift公式ライブラリ宣言 の実装から来ています。
目前在 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)
}
}
}
宏的实现代码(点击查看)
/// 声明
@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))
}
"""
]
}
}
@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() { }
}
宏的实现代码(点击查看)
/// 声明
@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"]
}
}
@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 {}
宏的实现代码(点击查看)
/// 声明
@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)]
}
}
怎么自己创建宏
写宏的准备工作
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
。現時点ではマクロはまだベータテストの段階であり、今後Appleがマクロを改良する可能性もあり、今後も注目して更新していきたいと思います。
この記事はmdniceマルチプラットフォームによって公開されています