Swift Macro系列3-实践篇attached(accessor) 等
Contents
本文是《Swift Macro系列》的第三篇,旨在通过详细的步骤和代码示例,指导读者如何设计和实现@attached(accessor)
,attached(memberAttribute)
,@attached(conformance)
和@attached(member)
宏。
@attached(accessor)
accessor 意为存取器,当关联到属性后,可以操作其 get
、set
、willSet
和 didSet
方法。
假设我们有一个 Point 类型,其中的 x、y参数都是希望可以直接操作字典进行赋值和获取的,每增加一个参数都需要重复编写 get 和 set 实现,也没法通过 property wrapper 访问其他的存储属性来简化代码。
struct Point {
var x: Int = 1
var y: Int = 2
}
这种场景下 @attached(accessor)
宏就能发挥作用,通过添加宏,预期的展开结果如下图所示:
源码AST结构图
-
SourceFile: 这是AST的根节点,代表整个源文件。
-
CodeBlockItemList: 这个节点包含了源文件中的所有代码块项。
-
CodeBlockItem: 这是
CodeBlockItemList
中的一个元素,代表一个代码块项。 -
StructDecl: 这个节点表示一个结构体声明。在这个例子中,它声明了一个名为
Point
的结构体。 -
AttributeList 和 DeclModifierList: 这两个节点分别表示属性列表和声明修饰符列表。在这个例子中,它们都是空的,因为
Point
结构体没有特殊的属性或修饰符。 -
struct Point: 这是结构体的名称。
-
MemberBlock: 这个节点包含了结构体成员的代码块。
-
MemberBlockItemList: 这个节点包含了结构体成员的列表。
-
MemberBlockItem: 这是
MemberBlockItemList
中的一个元素,代表一个成员。 -
VariableDecl: 这个节点表示一个变量声明。
-
AttributeList 和 DeclModifierList: 同样,这两个节点表示属性列表和声明修饰符列表,在这个例子中也是空的。
-
var: 这是变量声明的关键字。
-
PatternBindingList 和 PatternBinding: 这些节点表示模式绑定列表和模式绑定。在这个例子中,它们表示变量的名称和类型。
-
IdentifierPattern: 这个节点表示变量的名称,例如
x
和y
。 -
TypeAnnotation: 这个节点表示变量的类型注解。
-
IdentifierType: 这个节点表示类型标识符,例如
Int
。 -
InitializerClause: 这个节点表示变量的初始化子句。
-
IntegerLiteralExpr: 这个节点表示整数字面量表达式,例如
1
和2
,它们是变量x
和y
的初始值。
整个结构体Point
包含两个属性x
和y
,它们都是Int
类型,并且分别被初始化为1
和2
。这个AST展示了Swift代码的结构化表示,是后续撰写宏实现的基础。
创建expansion函数
public struct DictionaryStoragePropertyMacro: AccessorMacro {
public static func expansion<
Context: MacroExpansionContext,
Declaration: DeclSyntaxProtocol
>(
of node: AttributeSyntax,
providingAccessorsOf declaration: Declaration,
in context: Context
) throws -> [AccessorDeclSyntax] {
//添加code
}
由于添加DictionaryStoragePropertyMacro
宏的目标是对变量添加计算属性的get/set方法,所以declaration找到VariableDeclSyntax,VariableDeclSyntax 表示变量声明,对应于示例中的var x: Int = 1
。
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
binding.accessorBlock == nil,
let type = binding.typeAnnotation?.type
else {
return []
}
varDecl.bindings类型是PatternBindingListSyntax,通过查找第一个元素,获得标识符(identifier,例如y)和类型(type,例如Int)
// Ignore the "_storage" variable.
if identifier.text == "_storage" {
return []
}
guard let defaultValue = binding.initializer?.value else {
throw CustomError.message("stored property must have an initializer")
}
忽略内置添加的存储属性_storage
,否则就会为_storage
也添加计算属性;并且_storage
必须有初始值。
return [
"""
get {
_storage[\(literal: identifier.text), default: \(defaultValue)] as! \(type)
}
""",
"""
set {
_storage[\(literal: identifier.text)] = newValue
}
"""
]
返回需要添加的计算属性
虽然上面代码优化了字典参数的写法,但也引入了另外一个问题:所有属性前面必须加上 @DictionaryStorageProperty
模板代码,这又造成了重复编码,尤其是对含有大量属性的类型声明。要解决这个问题,需要引入下面这个新角色 @attached(memberAttribute)
。
@attached(memberAttribute)
这个角色可以为类型或扩展添加新特性。要解决上面模板代码的问题。
设计流程是这样的,先使用@DictionaryStorage
宏修饰Point
整体,然后展开后添加下面三项内容,对每个属性加@DictionaryStorageProperty
宏,增加_storage
属性。
接着,@DictionaryStorageProperty
宏在展开时添加计算属性,以实现完整功能。
看下一下代码具体实现,我们新建一个宏:
extension DictionaryStorageMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
guard let property = member.as(VariableDeclSyntax.self),
property.isStoredProperty
else {
return []
}
return [
AttributeSyntax(
leadingTrivia: [.newlines(1), .spaces(2)],
attributeName: IdentifierTypeSyntax(
name: .identifier("DictionaryStorageProperty")
)
)
]
}
}
判断成员是变量,且是存储属性;添加新的声明标识AttributeSyntax,即宏@DictionaryStorageProperty,并配置格式:在新的一行,2个空格。这里注意的是此expansion函数,有member参数,参数类型: some DeclSyntaxProtocol,在宏的扩展过程中,member 参数用于表示当前正在处理的成员声明。宏可以根据这个成员声明来生成或修改代码。
@attached(member)
member 角色可以为类型、扩展添加初始化方法、参数等新的声明,为类和结构体添加存储属性,甚至为枚举添加新的 case。
下面代码预期实现@CaseDetection宏,为枚举类型的每个case添加检测判断。
实现
public enum CaseDetectionMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
declaration.memberBlock.members
.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
.map { $0.elements.first!.name }
.map { ($0, $0.initialUppercased) }
.map { original, uppercased in
"""
var is\(raw: uppercased): Bool {
if case .\(raw: original) = self {
return true
}
return false
}
"""
}
}
}
具体步骤如下:
-
compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
:过滤并转换为EnumCaseDeclSyntax
类型,忽略不符合条件的声明。 -
.map { $0.elements.first!.name }
:提取每个枚举情况的名称。 -
.map { ($0, $0.initialUppercased) }
:将枚举名称转换为元组,包含原始名称和首字母大写的名称。 -
.map { original, uppercased in ... }
:生成一个字符串模板,定义一个布尔属性,用于检测枚举实例是否为特定枚举情况。- 模板内容:
var is\(raw: uppercased): Bool { if case .\(raw: original) = self { return true } return false }
- 这个模板会生成类似
var isFoo: Bool { ... }
的属性,用于检测枚举实例是否为.foo
情况。
- 模板内容:
代码片段通过遍历枚举情况,生成一组布尔属性,用于检测枚举实例是否为特定枚举情况。
@attached(conformance)
为关联的类型或扩展添加新的协议遵循;接下来,看一个具体例子,预期对类型实现OptionSet
协议;
OptionSet
协议在 Swift 中用于表示一组可以组合的选项。它类似于位掩码(bitmask),允许你通过组合不同的选项来创建复合选项。
struct ShippingOptions: OptionSet {
let rawValue: Int
static let nextDay = ShippingOptions(rawValue: 1 << 0)
static let secondDay = ShippingOptions(rawValue: 1 << 1)
static let priority = ShippingOptions(rawValue: 1 << 2)
static let standard = ShippingOptions(rawValue: 1 << 3)
}
// 使用示例
var options: ShippingOptions = [.nextDay, .priority]
if options.contains(.nextDay) {
print("Next day shipping is selected.")
}
options.insert(.standard)
print("Current options: \(options)")
设计@MyOptionSet宏,展开效果
extension OptionSetMacro: 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] {
// Decode the expansion arguments.
guard let (structDecl, _, _) = decodeExpansion(of: node, attachedTo: declaration, in: context) else {
return []
}
// If there is an explicit conformance to OptionSet already, don't add one.
if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypes,
inheritedTypes.contains(where: { inherited in inherited.type.trimmedDescription == "OptionSet" })
{
return []
}
return [try ExtensionDeclSyntax("extension \(type): OptionSet {}")]
}
}
- 代码实现了一个宏扩展,用于为符合条件的结构体添加 OptionSet 协议的扩展
- 调用 decodeExpansion 方法解码宏扩展参数,确保宏正确附加到结构体声明。
- 检查结构体是否已经显式遵循 OptionSet 协议。如果已经遵循,返回空数组,不添加新的扩展。
- 如果结构体未遵循 OptionSet 协议,生成一个新的扩展声明,使其遵循 OptionSet 协议。返回包含该扩展声明的数组。
角色组合
在上面的 MyOptionSet
宏的例子中,需要使用多个关联宏角色的组合共同对代码进行优化,才能实现完整功能,上面代码为了简化说明,只是列出了实现ExtensionMacro宏的部分
@attached(member, names: arbitrary)
@attached(extension, conformances: OptionSet)
public macro MyOptionSet<RawType>() = #externalMacro(module: "MacrosImplementation", type: "OptionSetMacro")
关于角色组合的使用需要满足下面几个原则:
- 一个宏可以有多个关联角色(attached roles)
- Swift 会在宏使用的地方将所有适用的角色都还原成对应实现
- 在使用的地方,至少需要有一个角色满足场景