Swift 宏系统解析(二):@freestanding(declaration)
在上一篇文章中,我们了解了 @freestanding(expression) 表达式宏如何消除表达式层面的重复代码。而 @freestanding(declaration) 声明宏能力更强——它能生成完整的类型、函数、属性等声明,从结构层面解决批量代码的重复问题。
为什么需要它
实现不同维度的数组类型,是声明宏的经典应用场景:
// 二维数组
public struct Array2D<Element>: Collection {
public struct Index: Hashable, Comparable { var storageIndex: Int }
var storage: [Element]
var width1: Int
public func makeIndex(_ i0: Int, _ i1: Int) -> Index {
Index(storageIndex: i0 * width1 + i1)
}
public subscript (_ i0: Int, _ i1: Int) -> Element {
get { self[makeIndex(i0, i1)] }
set { self[makeIndex(i0, i1)] = newValue }
}
public subscript (_ i: Index) -> Element {
get { storage[i.storageIndex] }
set { storage[i.storageIndex] = newValue }
}
}
// 三维数组 - 结构几乎相同
public struct Array3D<Element>: Collection {
public struct Index: Hashable, Comparable { var storageIndex: Int }
var storage: [Element]
var width1, width2: Int
public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index {
Index(storageIndex: (i0 * width1 + i1) * width2 + i2)
}
// ... 同样的下标实现
}
Array2D 和 Array3D 结构高度同构,唯一区别是维度数量。泛型和协议无法解决这个问题——维度数是类型层面的差异,不是运行时参数。
声明宏给出了完美解决方案:
#makeArrayND(n: 2) // 生成 Array2D<Element>
#makeArrayND(n: 3) // 生成 Array3D<Element>
#makeArrayND(n: 4) // 生成 Array4D<Element>
核心特性
| 特性 | 说明 |
|---|---|
| 独立存在 | 以 # 前缀调用,可放置在任何允许声明的位置 |
| 生成声明 | 输出完整的 Swift 声明(结构体、类、枚举、函数等) |
| 编译期展开 | 零运行时开销 |
| 命名审计 | 必须显式声明引入的名称范围 |
| 类型安全 | 展开后经过完整的 Swift 类型检查 |
与表达式宏的对比:
| 宏类型 | 遵循协议 | 返回类型 | 用途 |
|---|---|---|---|
@freestanding(expression) |
ExpressionMacro |
ExprSyntax |
生成表达式 |
@freestanding(declaration) |
DeclarationMacro |
[DeclSyntax] |
生成声明 |
#makeArrayND 实现
宏声明
/// 声明一个 N 维数组类型,命名为 Array<n>D
@freestanding(declaration, names: arbitrary)
public macro makeArrayND(n: Int) =
#externalMacro(module: "SimpleMacrosPlugin", type: "MakeArrayNDMacro")
names: arbitrary 表示宏可以生成任意名称,因为类型名由参数 n 动态决定。
宏实现
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct MakeArrayNDMacro: DeclarationMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// 1. 解析参数 n
guard let nArg = node.arguments.first?.expression.as(IntegerLiteralExprSyntax.self),
let n = Int(nArg.literal.text), n >= 1 else {
throw MacroError.message("n must be a positive integer literal")
}
// 2. 生成维度相关的变量名
let widthNames = (1..<n).map { "width\($0)" }
let indexParams = (0..<n).map { "i\($0)" }
// 3. 递推生成 storageIndex 计算表达式
// n=2: i0 * width1 + i1
// n=3: (i0 * width1 + i1) * width2 + i2
var indexExpr = "i0"
for i in 1..<n {
indexExpr = "(\(indexExpr) * width\(i) + i\(i))"
}
// 4. 拼接属性和参数
let storageProperties = widthNames
.map { "var \($0): Int" }
.joined(separator: "\n ")
let makeIndexParams = indexParams
.map { "_ \($0): Int" }
.joined(separator: ", ")
// 5. 生成完整结构体
let structDecl: DeclSyntax = """
public struct Array\(raw: n)D<Element>: Collection {
public struct Index: Hashable, Comparable {
var storageIndex: Int
public static func < (lhs: Index, rhs: Index) -> Bool {
lhs.storageIndex < rhs.storageIndex
}
}
var storage: [Element]
\(raw: storageProperties)
public var startIndex: Index { Index(storageIndex: 0) }
public var endIndex: Index { Index(storageIndex: storage.count) }
public func index(after i: Index) -> Index {
Index(storageIndex: i.storageIndex + 1)
}
public func makeIndex(\(raw: makeIndexParams)) -> Index {
Index(storageIndex: \(raw: indexExpr))
}
public subscript (\(raw: makeIndexParams)) -> Element {
get { self[makeIndex(\(raw: indexParams.joined(separator: ", ")))] }
set { self[makeIndex(\(raw: indexParams.joined(separator: ", ")))] = newValue }
}
public subscript (_ i: Index) -> Element {
get { storage[i.storageIndex] }
set { storage[i.storageIndex] = newValue }
}
}
"""
return [structDecl]
}
}
关键点:
- 实现
DeclarationMacro协议 - 返回
[DeclSyntax]数组(可生成多个声明) - 使用字符串插值动态生成代码
\raw:语法直接插入原始字符串
展开结果
#makeArrayND(n: 2) 展开后:
public struct Array2D<Element>: Collection {
public struct Index: Hashable, Comparable {
var storageIndex: Int
public static func < (lhs: Index, rhs: Index) -> Bool {
lhs.storageIndex < rhs.storageIndex
}
}
var storage: [Element]
var width1: Int
public var startIndex: Index { Index(storageIndex: 0) }
public var endIndex: Index { Index(storageIndex: storage.count) }
public func index(after i: Index) -> Index {
Index(storageIndex: i.storageIndex + 1)
}
public func makeIndex(_ i0: Int, _ i1: Int) -> Index {
Index(storageIndex: (i0 * width1 + i1))
}
public subscript (_ i0: Int, _ i1: Int) -> Element {
get { self[makeIndex(i0, i1)] }
set { self[makeIndex(i0, i1)] = newValue }
}
public subscript (_ i: Index) -> Element {
get { storage[i.storageIndex] }
set { storage[i.storageIndex] = newValue }
}
}
命名权限机制
声明宏必须显式声明引入的名称范围,有三种模式:
| 模式 | 语法 | 说明 | 适用场景 |
|---|---|---|---|
| 任意命名 | names: arbitrary |
可生成任意名称 | 名称由参数动态决定 |
| 前缀命名 | names: prefixed(Array) |
名称必须以指定前缀开头 | 系列类型生成 |
| 固定命名 | names: named(Array2D, Array3D) |
只能生成指定名称 | 名称固定的场景 |
#makeArrayND 使用 arbitrary,因为它需要根据参数生成 Array2D、Array3D 等不同名称。
Swift 6 注意事项
在 Swift 6 中,arbitrary 命名权限的宏不能在全局作用域使用:
// ❌ 编译错误:'declaration' macros are not allowed to introduce arbitrary names at global scope
#makeArrayND(n: 2)
解决方案是将宏调用放在枚举或结构体内部:
enum ArrayTypes {
#makeArrayND(n: 2)
#makeArrayND(n: 3)
}
// 使用时通过外层类型访问
let array = ArrayTypes.Array2D<Int>(storage: Array(repeating: 0, count: 6), width1: 3)
这是因为 Swift 6 加强了对宏引入名称的安全检查,arbitrary 权限过大,需要限制在封闭作用域内。
测试
使用 SwiftSyntaxMacrosTestSupport 测试声明宏:
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
final class MakeArrayNDMacroTests: XCTestCase {
func testMakeArray2D() throws {
assertMacroExpansion(
"""
#makeArrayND(n: 2)
""",
expandedSource: """
public struct Array2D<Element>: Collection {
// ... 展开后的完整代码
}
""",
macros: ["makeArrayND": MakeArrayNDMacro.self]
)
}
func testInvalidN() throws {
assertMacroExpansion(
"""
#makeArrayND(n: 0)
""",
expandedSource: """
#makeArrayND(n: 0)
""",
diagnostics: [
DiagnosticSpec(message: "n must be a positive integer literal", line: 1, column: 1)
],
macros: ["makeArrayND": MakeArrayNDMacro.self]
)
}
}
最佳实践
- 优先使用
prefixed或named:权限更小,代码更可预测 - 校验输入参数:在宏实现中检查参数合法性,抛出清晰的错误信息
- 生成可读代码:格式规范,方便开发者查看展开结果
- 避免过度使用:泛型、协议能解决的问题不需要用宏
小结
@freestanding(declaration) 将元编程能力从表达式层面提升到声明层面。通过 #makeArrayND 示例,我们看到它如何将大量重复的样板代码简化为一行宏调用,同时保持类型安全和零运行时开销。
下一篇,我们将探索 @attached 宏——它能附加到已有声明上,扩展其功能。