Shaula Blog

美丽的东西都是肤浅的

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)
    }
    // ... 同样的下标实现
}

Array2DArray3D 结构高度同构,唯一区别是维度数量。泛型和协议无法解决这个问题——维度数是类型层面的差异,不是运行时参数。

声明宏给出了完美解决方案:

#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,因为它需要根据参数生成 Array2DArray3D 等不同名称。

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]
        )
    }
}

最佳实践

  1. 优先使用 prefixednamed:权限更小,代码更可预测
  2. 校验输入参数:在宏实现中检查参数合法性,抛出清晰的错误信息
  3. 生成可读代码:格式规范,方便开发者查看展开结果
  4. 避免过度使用:泛型、协议能解决的问题不需要用宏

小结

@freestanding(declaration) 将元编程能力从表达式层面提升到声明层面。通过 #makeArrayND 示例,我们看到它如何将大量重复的样板代码简化为一行宏调用,同时保持类型安全和零运行时开销。

下一篇,我们将探索 @attached 宏——它能附加到已有声明上,扩展其功能。