一、问题的引出
想象这样一个场景:你正在开发一个网络请求框架,每定义一个 API 接口,都要手动编写大量模板代码:
swift
// 定义用户接口
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 核心思想
元编程的核心是将重复的模式抽象为规则,让程序自动处理这些模式。
比如上面的例子,我们只需要定义:
swift
@API(endpoint: "/user", method: .get)
struct UserAPI {
typealias Response = User
}
编译器就能自动生成所有实现代码。
2.3 元编程解决什么问题
| 问题 | 传统方式 | 元编程方式 |
|---|---|---|
| 代码重复 | 复制粘贴 | 编译器自动生成 |
| 样板代码 | 手动编写 | 编译时展开 |
| 跨模块一致性 | 依赖约定 | 编译时强制 |
| 运行时错误 | 测试发现 | 编译时发现 |
| 代码维护 | 多处修改 | 修改宏定义即可 |
三、Swift 中的元编程形式
Swift 提供了多种元编程机制,按历史演进:
3.1 泛型(Generics)
最基本的元编程形式,通过类型参数化生成特定类型的代码。
swift
// 编写一次,编译器为每个类型生成特化版本
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)
运行时检查类型的元数据。
swift
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 大量使用。
swift
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
为属性添加统一的行为逻辑。
swift
@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 的核心技术。
swift
@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的过程(有遇到坑🕳️)。
js
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宏的实现原理
js
源代码 (.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
5.2 项目中引用
个人集成时遇到宏模块导入失败的问题,所以这里单独写下步骤。
- 设置为 Swift 5.9 或更高
创建新的 Workspace
如果用 CocoaPods 已经有一个 workspace直接修改;没有的话需要创建一个新 workspace。
bash
# 进入你的项目目录
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
-
在 Xcode 项目导航器中,点击蓝色项目文件
-
选择 PROJECT(不是 TARGETS)
-
选择 Package Dependencies 标签
-
点击 + 按钮
-
点击左下角 Add Local...
-
导航到你的
MyMacros文件夹(包含 Package.swift 的那个) -
点击 Add Package
-
在弹出的对话框中:
- 确保勾选你的 App Target
- 点击 Add Package
- 验证添加成功
- 项目导航器中应该出现
Package Dependencies组 - 里面有你的
MyMacros和相关依赖 - Target → General → Frameworks, Libraries, and Embedded Content 中应该有
MyMacros
六、系统内置宏示例
6.1 #warning 和 #error
swift
// 编译时检查
#if DEBUG
#warning("这是调试版本")
#endif
// 条件编译错误
#if !os(iOS)
#error("此代码只能在 iOS 上运行")
#endif
6.2 #available
swift
// 平台版本检查
if #available(iOS 17.0, *) {
// 使用 iOS 17 新 API
} else {
// 降级方案
}
6.3 #selector
swift
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
swift
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 新增系统宏
swift
// #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 日志宏
问题:手动写日志包含文件、行号等信息太繁琐。
swift
// 宏声明
@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 元编程的演进
yaml
Swift 1.0: 泛型
Swift 2.0: 协议扩展
Swift 4.0: KeyPath
Swift 5.1: Property Wrapper
Swift 5.4: Result Builder
Swift 5.9: Macros ← 元编程的新时代
核心要点
- 元编程的本质是用代码生成代码,消除重复模式
- Swift 提供多种元编程机制,从编译时到运行时,从简单到复杂
- 宏是编译时代码生成,在语法树层面操作,保证类型安全
- 自定义宏需要两部分:声明(对外接口)和实现(生成逻辑)
- 合理使用可以大幅减少样板代码,提升代码质量和开发效率
元编程不是银弹,但在合适的场景下,它能让我们写更少的代码,做更多的事情,同时保持类型安全和编译时检查。
进一步阅读文章: # bilibili-Macro 在业务开发中的探索与实践