1161 字
6 分钟
Swift Macros
2025-03-12

宏在编译时转换你的源代码,让你避免手动编写重复代码。在编译过程中,Swift 会先展开代码中的所有宏,再按常规流程构建代码。

宏扩展示意图

宏扩展始终是增量操作:宏只添加新代码,从不删除或修改现有代码。

宏的输入和宏扩展的输出都会经过检查,确保其语法是合法的 Swift 代码。同样地,传递给宏的值和宏生成的代码中的值也会经过类型检查。此外,如果宏实现在展开时遇到错误,编译器会将其视为编译错误。这些保证使得使用宏的代码更易推理,也更容易发现诸如宏使用不当或实现存在缺陷等问题。

Swift 有两种宏:

  • 独立宏(Freestanding macros)独立存在,不依附于任何声明。
  • 附加宏(Attached macros)修饰它们所依附的声明。

两者的调用方式略有不同,但都遵循相同的扩展模型,且实现方式也相同。下文将详细阐述这两种宏。

独立宏#

调用独立宏时,需在宏名前添加井号(#),参数写在宏名后的括号中。例如:

func myFunction() {
    print("当前正在执行 \(#function)")
    #warning("有问题")
}

第一行中的 #function 调用了 Swift 标准库中的 function() 宏。编译时,Swift 会调用该宏的实现,将 #function 替换为当前函数名。运行此代码并调用 myFunction() 时,会输出 “当前正在执行 myFunction()“。第二行中的 #warning 调用了 Swift 标准库的 warning(_:) 宏,用于生成自定义编译时警告。

独立宏可以像 #function 一样生成值,也可以像 #warning 一样在编译时执行操作。

附加宏#

调用附加宏时,需在宏名前添加 @,参数写在宏名后的括号中。

附加宏会修改它们所依附的声明,例如定义新方法或添加协议一致性。

例如,考虑以下未使用宏的代码:

struct SundaeToppings: OptionSet {
    let rawValue: Int
    static let nuts = SundaeToppings(rawValue: 1 << 0)
    static let cherry = SundaeToppings(rawValue: 1 << 1)
    static let fudge = SundaeToppings(rawValue: 1 << 2)
}

这段代码中,每个选项都需手动调用初始化器,既重复又易错。使用宏后的版本:

@OptionSet<Int>
struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }
}

@OptionSet 宏会读取私有枚举中的 case,为每个选项生成常量,并添加对 OptionSet 协议的遵循。展开后的代码:

struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }

    typealias RawValue = Int
    var rawValue: RawValue
    init() { self.rawValue = 0 }
    init(rawValue: RawValue) { self.rawValue = rawValue }
    static let nuts: Self = Self(rawValue: 1 << Options.nuts.rawValue)
    static let cherry: Self = Self(rawValue: 1 << Options.cherry.rawValue)
    static let fudge: Self = Self(rawValue: 1 << Options.fudge.rawValue)
}
extension SundaeToppings: OptionSet { }

宏声明#

宏的声明与实现分离。声明使用 macro 关键字引入:

@attached(member)
@attached(extension, conformances: OptionSet)
public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")
  • @attached 属性指定宏角色(如添加成员或扩展)
  • #externalMacro 指定实现位置
  • 附加宏使用大驼峰命名,独立宏使用小驼峰命名
  • 必须声明为 public

宏扩展流程#

  1. 构建抽象语法树(AST):编译器将源代码转换为内存中的树形结构
  2. 发送部分 AST 到宏实现:仅包含与宏调用相关的节点
  3. 生成新 AST:宏实现返回扩展后的代码结构
  4. 替换和继续编译:编译器使用扩展后的代码继续编译流程

示例:#fourCharacterCode("ABCD") 的扩展过程:

let magicNumber = 1145258561 as UInt32

实现宏#

需创建两个组件:

  1. 执行扩展的宏类型
  2. 声明宏的库

使用 Swift Package Manager 创建模板:

swift package init --type macro

Package.swift 配置示例:

// swift-tools-version: 5.9
import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MyPackage",
    platforms: [.macOS(.v10.15)],
    dependencies: [
        .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0")
    ],
    targets: [
        .macro(
            name: "MyMacros",
            dependencies: [
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        .target(name: "MyLib", dependencies: ["MyMacros"])
    ]
)

四字符编码宏实现示例:

public struct FourCharacterCode: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        guard let literal = node.argumentList.first?.expression,
              let string = literal.as(StringLiteralExprSyntax.self)?.segments.first else {
            throw CustomError.message("需要静态字符串")
        }
        
        guard let code = calculateCode(string) else {
            throw CustomError.message("无效四字符编码")
        }
        
        return "\(raw: code) as UInt32"
    }
}

调试与测试#

利用 SwiftSyntax 的测试能力:

let source: SourceFileSyntax = """
let abcd = #fourCharacterCode("ABCD")
"""

let context = BasicMacroExpansionContext()
let transformed = source.expand(macros: ["fourCharacterCode": FourCharacterCode.self], in: context)

assert(transformed.description == "let abcd = 1145258561 as UInt32")

通过预编译时的代码转换,Swift 宏系统在保持类型安全的同时,显著提升了代码的简洁性和可维护性。开发者可以借此消除大量模板代码,同时享受编译时的安全保障。

Swift Macros
https://blog.lpkt.cn/posts/swift-macros/
作者
lollipopkit
发布于
2025-03-12
许可协议
CC BY-NC-SA 4.0