You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 18:55:52