宏在编译时转换你的源代码,让你避免手动编写重复代码。在编译过程中,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
宏扩展流程
- 构建抽象语法树(AST):编译器将源代码转换为内存中的树形结构
- 发送部分 AST 到宏实现:仅包含与宏调用相关的节点
- 生成新 AST:宏实现返回扩展后的代码结构
- 替换和继续编译:编译器使用扩展后的代码继续编译流程
示例:#fourCharacterCode("ABCD")
的扩展过程:
let magicNumber = 1145258561 as UInt32
实现宏
需创建两个组件:
- 执行扩展的宏类型
- 声明宏的库
使用 Swift Package Manager 创建模板:
swift package init --type macro
Package.swift 配置示例:
import PackageDescriptionimport 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 宏系统在保持类型安全的同时,显著提升了代码的简洁性和可维护性。开发者可以借此消除大量模板代码,同时享受编译时的安全保障。