Swift 元编程-Macro

24 阅读6分钟

一、问题的引出

想象这样一个场景:你正在开发一个网络请求框架,每定义一个 API 接口,都要手动编写大量模板代码:

// 定义用户接口
struct UserAPI {
    // 需要手动写 URL
    var url: String { "https://api.example.com/user" }
    
    // 需要手动写请求方法
    var method: HTTPMethod { .get }
    
    // 需要手动写参数编码
    var parameters: [String: Any]? { nil }
    
    // 需要手动写响应解析
    func parse(_ data: Data) throws -> User {
        return try JSONDecoder().decode(User.self, from: data)
    }
    
    // 需要手动写错误处理
    func handleError(_ error: Error) -> APIError {
        return APIError.networkError(error)
    }
}

每个 API 都要重复这些模式,代码冗长且容易出错。当项目有几十个接口时,维护成本急剧上升。

能不能让编译器自动生成这些代码?

这就引出了元编程的概念。

二、什么是元编程

2.1 定义

元编程(Metaprogramming)是指编写能够操作、生成或转换其他程序代码的程序。简单说,就是用代码来生成代码

传统编程:开发者写代码 → 编译器编译 → 可执行程序
元编程:开发者写规则 → 代码生成器 → 生成代码 → 编译器编译 → 可执行程序

2.2 核心思想

元编程的核心是将重复的模式抽象为规则,让程序自动处理这些模式

比如上面的例子,我们只需要定义:

@API(endpoint: "/user", method: .get)
struct UserAPI {
    typealias Response = User
}

编译器就能自动生成所有实现代码。

2.3 元编程解决什么问题

问题传统方式元编程方式
代码重复复制粘贴编译器自动生成
样板代码手动编写编译时展开
跨模块一致性依赖约定编译时强制
运行时错误测试发现编译时发现
代码维护多处修改修改宏定义即可

三、Swift 中的元编程形式

Swift 提供了多种元编程机制,按历史演进:

3.1 泛型(Generics)

最基本的元编程形式,通过类型参数化生成特定类型的代码。

// 编写一次,编译器为每个类型生成特化版本
func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 1, y = 2
swapValues(&x, &y)  // 编译器生成 swapValues<Int>

3.2 反射(Mirror)

运行时检查类型的元数据。

struct User {
    let name: String
    let age: Int
}

let user = User(name: "张三", age: 25)
let mirror = Mirror(reflecting: user)

for child in mirror.children {
    print("\(child.label ?? "未知"): \(child.value)")
}
// 输出:
// name: 张三
// age: 25

限制:只读,无法修改,运行时开销。

3.3 KeyPath

类型安全的属性访问路径,SwiftUI 大量使用。

struct Person {
    var name: String
    var address: Address
}

struct Address {
    var city: String
}

// KeyPath 本身就是类型的元数据
let cityPath = \Person.address.city
var person = Person(name: "李四", address: Address(city: "北京"))

// 通过 KeyPath 读写值
person[keyPath: cityPath] = "上海"
print(person[keyPath: cityPath])  // 上海

3.4 Property Wrapper

为属性添加统一的行为逻辑。

@propertyWrapper
struct UserDefaultsStorage<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

struct Settings {
    @UserDefaultsStorage(key: "username", defaultValue: "")
    var username: String
    
    @UserDefaultsStorage(key: "launchCount", defaultValue: 0)
    var launchCount: Int
}
// 编译器自动生成 getter/setter 代码

3.5 Result Builder

构建声明式 DSL,SwiftUI 的核心技术。

@resultBuilder
struct HTMLBuilder {
    static func buildBlock(_ components: String...) -> String {
        components.joined(separator: "\n")
    }
}

func html(@HTMLBuilder content: () -> String) -> String {
    """
    <!DOCTYPE html>
    <html>
    \(content())
    </html>
    """
}

// 声明式构建 HTML
let page = html {
    "<head><title>Hello</title></head>"
    "<body>"
    "<h1>Welcome</h1>"
    "</body>"
}

3.6 Swift Macro(5.9+)

最新的编译时代码生成技术,由于已有很多文章介绍过不再赘述,我这里记录下demo的过程(有遇到坑🕳️)。

Swift Macro
├── 独立宏 (Freestanding Macro)          // 使用 # 前缀
│   ├── 表达式宏 (Expression Macro)       // #stringify, #warning, #error
│   └── 声明宏 (Declaration Macro)        // Swift 目前未开放直接定义,但系统有 #available 等
│
└── 附加宏 (Attached Macro)              // 使用 @ 前缀
    ├── 成员宏 (Member Macro)            // @CaseDetection, @CustomCodable
    ├── 成员属性宏 (Member Attribute Macro) // 给生成的成员添加属性
    ├── 访问器宏 (Accessor Macro)         // @UserDefault, 自动生成 get/set
    ├── 扩展宏 (Extension Macro)          // 生成扩展
    ├── 协议一致性宏 (Conformance Macro)   // 自动添加协议遵循
    └── 对等宏 (Peer Macro)              // 生成与当前声明并列的代码(如生成对应的其他类型)

四、Swift宏的实现原理

源代码 (.swift)
      │
      ▼
┌──────────────┐
│   词法分析    │  将字符流转换为 Token 序列
│   (Lexer)    │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   语法分析    │  将 Token 序列构建为抽象语法树 (AST)
│   (Parser)   │
└──────┬───────┘
       │
       ▼
┌─────────────────────────────────────────┐
│            宏展开 (Macro Expansion)     │
│  ┌──────────────────────────────────┐   │
│  │ 识别 AST 中的宏调用节点           │   │
│  │ 加载对应的宏插件 (CompilerPlugin) │   │
│  │ 调用宏的 expansion 方法,传入节点  │   │
│  │ 返回新生成的 AST 节点             │   │
│  │ 替换原始宏调用节点                 │   │
│  └──────────────────────────────────┘   │
└──────┬──────────────────────────────────┘
       │
       ▼
┌──────────────┐
│   语义分析    │  类型检查、符号绑定等
│   (Sema)     │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  SIL 生成    │  Swift Intermediate Language
│  (SILGen)    │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  LLVM IR     │
│  生成与优化  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   机器码     │
│    (0/1)     │
└──────────────┘

五、已有项目中集成Macro

5.1 创建自定义宏Package

参考juejin.cn/post/724472…

5.2 项目中引用

个人集成时遇到宏模块导入失败的问题,所以这里单独写下步骤。

  • 设置为 Swift 5.9 或更高

创建新的 Workspace

如果用 CocoaPods 已经有一个 workspace直接修改;没有的话需要创建一个新 workspace。

# 进入你的项目目录
cd /path/to/YourProject/YourApp

# 创建 workspace 文件夹
mkdir -p YourAppWithMacro.xcworkspace

# 创建 contents.xcworkspacedata 文件
cat > YourAppWithMacro.xcworkspace/contents.xcworkspacedata << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:YourApp.xcodeproj">
   </FileRef>
   <FileRef
      location = "group:../../MyMacros">
   </FileRef>
</Workspace>
EOF

# 打开 workspace
open YourAppWithMacro.xcworkspace

在 Xcode 中添加宏包

3.1 添加本地 Swift Package

  1. 在 Xcode 项目导航器中,点击蓝色项目文件

  2. 选择 PROJECT(不是 TARGETS)

  3. 选择 Package Dependencies 标签

  4. 点击  +  按钮

  5. 点击左下角 Add Local...

  6. 导航到你的 MyMacros 文件夹(包含 Package.swift 的那个)

  7. 点击 Add Package

  8. 在弹出的对话框中:

    • 确保勾选你的 App Target
    • 点击 Add Package
  • 验证添加成功
  1. 项目导航器中应该出现 Package Dependencies 组
  2. 里面有你的 MyMacros 和相关依赖
  3. Target → General → Frameworks, Libraries, and Embedded Content 中应该有 MyMacros

六、系统内置宏示例

6.1 #warning 和 #error

// 编译时检查
#if DEBUG
#warning("这是调试版本")
#endif

// 条件编译错误
#if !os(iOS)
#error("此代码只能在 iOS 上运行")
#endif

6.2 #available

// 平台版本检查
if #available(iOS 17.0, *) {
    // 使用 iOS 17 新 API
} else {
    // 降级方案
}

6.3 #selector

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 编译器验证方法存在
        let button = UIButton()
        button.addTarget(self, 
                        action: #selector(buttonTapped), 
                        for: .touchUpInside)
    }
    
    @objc func buttonTapped() {
        print("按钮被点击")
    }
}

6.4 #keyPath

class Person: NSObject {
    @objc dynamic var name: String
    @objc dynamic var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 类型安全的 KVO
let person = Person(name: "张三", age: 25)
person.observe(\.name) { object, change in
    print("名字改变了")
}

6.5 Swift 5.9 新增系统宏

// #Predicate - 类型安全的谓词
let predicate = #Predicate<Person> { 
    $0.age > 18 && $0.name.contains("张")
}

// #FileID - 模块内的文件标识
let fileID = #FileID  // MyModule/MyFile.swift

// #filePath - 完整文件路径
let path = #filePath  // /Users/xxx/MyFile.swift

七、自定义宏举例

7.1 日志宏

问题:手动写日志包含文件、行号等信息太繁琐。

// 宏声明
@freestanding(expression)
public macro LogDebug(_ message: String) -> Void = 
    #externalMacro(module: "MyMacrosMacros", type: "LogMacro")

// 宏实现
public struct LogMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        let message = node.arguments.first!.expression
        let file = context.location(of: node)?.file ?? "<unknown>"
        let line = context.location(of: node)?.line ?? 0
        
        return """
        print("[\(raw: file):\(raw: line)] \(message)")
        """
    }
}

// 使用
#LogDebug("用户登录成功")
// 编译时展开为:print("[MyFile.swift:42] 用户登录成功")

八、总结

Swift 元编程的演进

Swift 1.0: 泛型
Swift 2.0: 协议扩展
Swift 4.0: KeyPath
Swift 5.1: Property Wrapper
Swift 5.4: Result Builder  
Swift 5.9: Macros  元编程的新时代

核心要点

  1. 元编程的本质是用代码生成代码,消除重复模式
  2. Swift 提供多种元编程机制,从编译时到运行时,从简单到复杂
  3. 宏是编译时代码生成,在语法树层面操作,保证类型安全
  4. 自定义宏需要两部分:声明(对外接口)和实现(生成逻辑)
  5. 合理使用可以大幅减少样板代码,提升代码质量和开发效率

元编程不是银弹,但在合适的场景下,它能让我们写更少的代码,做更多的事情,同时保持类型安全和编译时检查。


进一步阅读文章: # bilibili-Macro 在业务开发中的探索与实践

# 开始写一个 Swift 宏