宏在编译时转换你的源代码,让你避免手动编写重复代码。在编译过程中,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 配置示例:
// 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 宏系统在保持类型安全的同时,显著提升了代码的简洁性和可维护性。开发者可以借此消除大量模板代码,同时享受编译时的安全保障。