Swift中如何为MemberMacro生成的嵌入结构体注入协议一致性?
问题
我正在Swift中实现MemberMacro,目前已完成以下功能:
- 为标注了宏的class创建一个嵌入struct,该struct拥有与class相同的属性
- 实现了从该struct初始化class的便利构造器,以及class转换为该struct的方法
现在需要实现一个需求:在宏调用时按需指定生成的嵌入struct需要遵循的协议,但不清楚如何在调用端声明这些协议。
当前宏调用仅通过@RecordInterfacable标注class,而MemberMacro的expansion(of:providingMembersOf:conformingTo:in:)方法虽包含conformingTo参数,但尚未找到在调用端传入所需协议的方式。
现有代码示例
标注宏的class
@RecordInterfacable @Observable class Model { let id: UUID var title: String init( id: UUID = UUID(), title: String ) { self.id = id self.title = title } }
宏展开后的期望代码
@Observable class Model { let id: UUID var title: String init( id: UUID = UUID(), title: String ) { self.id = id self.title = title } // 需要让该struct遵循调用时注入的协议 struct ModelRecord: Codable { // 这里的协议要从调用端传入 let id: UUID var title: String } init(from record: ModelRecord) { self.init( id: record.id, title: record.title ) } func convertToRecord() -> ModelRecord { ModelRecord( id: self.id, title: self.title ) } }
宏的实现代码
// RecordInterfacable 宏声明 @attached(member, names: arbitrary) public macro RecordInterfacable() = #externalMacro(module: "RecordInterfacableMacros", type: "RecordInterfacableMacro") // RecordInterfacableMacro 实现 public struct RecordInterfacableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax] { let classDecl = try assertClassDecl(for: declaration) let symbolName = try extractSymbolName(from: classDecl) /// 提取类的所有属性 let membersDeclSyntax = declaration .as(ClassDeclSyntax.self)? .memberBlock .members .compactMap { $0 .as(MemberBlockItemSyntax.self)? .decl .as(DeclSyntax.self) } let membersVariableDeclSyntax = membersDeclSyntax? .compactMap { $0.as(VariableDeclSyntax.self) } let memberBindingSpecifiers: [String]? = membersVariableDeclSyntax? .compactMap { member in guard let specifier = member.bindingSpecifier.text.split(separator: ".").last else { fatalError() } return "\(specifier)" } guard let memberBindingSpecifiers else { fatalError() } let identifierTexts = membersVariableDeclSyntax? .map { member in guard let id = member.bindings.compactMap { $0.as(PatternBindingSyntax)? .pattern .as(IdentifierPatternSyntax)? .identifier.text }.first else { fatalError() } return id } guard let identifierTexts else { fatalError() } let memberTypes = membersVariableDeclSyntax? .map { member in guard let type = member.bindings.compactMap { $0.as(PatternBindingSyntax)? .typeAnnotation? .type .as(IdentifierTypeSyntax)? .name.text }.first else { fatalError() } return type } guard let memberTypes else { fatalError() } var memberStrings = [String]() var initStrings = [String]() var varString = [String]() for i in 0..<identifierTexts.count { memberStrings.append("\(memberBindingSpecifiers[i]) \(identifierTexts[i]): \(memberTypes[i])") initStrings.append("\(identifierTexts[i]): record.\(identifierTexts[i])") varString.append("\(identifierTexts[i]): self.\(identifierTexts[i])") } let memberString = memberStrings.joined(separator: "\n") let initString = initStrings.joined(separator: ", ") return [ DeclSyntax( stringLiteral: """ struct \(symbolName)Record: Codable, FetchableRecord, PersistableRecord { \(memberString) } """ ), DeclSyntax( extendedGraphemeClusterLiteral: """ convenience init(from record: \(symbolName)Record) { self.init(\(initString)) } """ ), DeclSyntax( stringLiteral: """ var record: \(symbolName)Record { \(symbolName)Record(id: self.id, title: self.title) } """ ), ] } }
解决方案
要实现调用端指定协议的功能,需要修改宏的声明、调用方式,并在宏实现中处理传入的协议参数,具体步骤如下:
1. 修改宏的声明,添加参数支持
将宏声明改为接受可变数量的协议类型参数:
@attached(member, names: arbitrary) public macro RecordInterfacable(_ protocols: Type...) = #externalMacro(module: "RecordInterfacableMacros", type: "RecordInterfacableMacro")
2. 在调用端传入协议
标注class时直接在宏后指定需要遵循的协议,示例:
@RecordInterfacable(Codable, FetchableRecord, PersistableRecord) @Observable class Model { // ... 原有代码 ... }
如果不需要额外协议,可空调用:@RecordInterfacable()
3. 修改宏实现,处理传入的协议参数
在expansion方法中,从node(AttributeSyntax)提取传入的协议参数,替换到生成的struct协议列表中:
首先添加辅助方法提取宏参数中的协议:
private static func extractProtocols(from node: AttributeSyntax) -> [String] { guard let argumentList = node.arguments?.as(LabeledExprListSyntax.self) else { return [] } return argumentList.compactMap { expr in expr.expression.as(TypeSyntax.self)?.description.trimmingCharacters(in: .whitespacesAndNewlines) } }
然后修改生成struct的代码,动态拼接协议列表:
// 提取调用端传入的协议,可添加默认协议(如Codable) let inputProtocols = extractProtocols(from: node) let finalProtocols = inputProtocols.isEmpty ? ["Codable"] : inputProtocols let protocolString = finalProtocols.joined(separator: ", ") // 替换原有硬编码的协议部分 let recordStructDecl = DeclSyntax( stringLiteral: """ struct \(symbolName)Record: \(protocolString) { \(memberString) } """ )
完整修改后的宏实现片段
public struct RecordInterfacableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax] { let classDecl = try assertClassDecl(for: declaration) let symbolName = try extractSymbolName(from: classDecl) // ... 原有提取属性的代码保持不变 ... // 提取调用端传入的协议 let inputProtocols = extractProtocols(from: node) // 可选:添加默认协议,无传入时默认遵循Codable let finalProtocols = inputProtocols.isEmpty ? ["Codable"] : inputProtocols let protocolString = finalProtocols.joined(separator: ", ") var memberStrings = [String]() var initStrings = [String]() var varString = [String]() for i in 0..<identifierTexts.count { memberStrings.append("\(memberBindingSpecifiers[i]) \(identifierTexts[i]): \(memberTypes[i])") initStrings.append("\(identifierTexts[i]): record.\(identifierTexts[i])") varString.append("\(identifierTexts[i]): self.\(identifierTexts[i])") } let memberString = memberStrings.joined(separator: "\n") let initString = initStrings.joined(separator: ", ") return [ DeclSyntax( stringLiteral: """ struct \(symbolName)Record: \(protocolString) { \(memberString) } """ ), DeclSyntax( extendedGraphemeClusterLiteral: """ convenience init(from record: \(symbolName)Record) { self.init(\(initString)) } """ ), DeclSyntax( stringLiteral: """ var record: \(symbolName)Record { \(symbolName)Record(\(varString.joined(separator: ", "))) } """ ), ] } private static func extractProtocols(from node: AttributeSyntax) -> [String] { guard let argumentList = node.arguments?.as(LabeledExprListSyntax.self) else { return [] } return argumentList.compactMap { expr in expr.expression.as(TypeSyntax.self)?.description.trimmingCharacters(in: .whitespacesAndNewlines) } } }
注意事项
- 确保调用端传入的协议在当前作用域可见,否则宏展开后会出现编译错误
- 若需强制要求至少传入一个协议,可在宏实现中添加校验逻辑
- 方法参数
conformingTo用于指定宏扩展的类型需要遵循的协议,与本次需求无关,我们需从宏的参数中提取协议
内容的提问来源于stack exchange,提问作者appfrosch
相关产品推荐
相关产品推荐

