前言
没有宏之前的痛点
在 Swift 5.9 之前,我们经常遇到这些问题:
// 痛点 1:大量重复的样板代码
struct User: Codable, Equatable, Hashable {
let id: Int
let name: String
let email: String
// 手动实现 CodingKeys(如果属性名不匹配)
enum CodingKeys: String, CodingKey {
case id = "user_id"
case name = "user_name"
case email = "user_email"
}
// 有时需要手动实现 Equatable
static func == (lhs: User, rhs: User) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.email == rhs.email
}
// 手动实现 Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
hasher.combine(email)
}
}
// 痛点 2:调试信息获取困难
func debugLog(_ message: String, file: String = #file, line: Int = #line, function: String = #function) {
print("[\(file):\(line)] \(function) - \(message)")
}
// 痛点 3:缺乏编译时代码生成能力
// 只能通过运行时反射或代码生成工具(如 Sourcery)
Swift 宏带来的改变
// ✨ 使用宏后的优雅代码
// 1. 一行代码搞定
@Observable
class User {
var id: Int
var name: String
var email: String
}
// 2. 智能调试
#warning("TODO: Implement this feature")
let value = #unwrap(optionalValue, "Value should not be nil")
// 3. 编译时代码生成
@CodingKeys(using: .snakeCase)
struct User: Codable {
let userId: Int // 自动映射到 "user_id"
let userName: String // 自动映射到 "user_name"
}
基础概念
什么是 Swift 宏?
Swift 宏是一种编译时代码转换机制,它在编译阶段读取源代码,并生成新的代码插入到程序中。
┌─────────────────────────────────────────────────────────────────┐
│ 编译流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 源代码 │ -> │ 宏展开 │ -> │ 类型检查 │ -> │ 代码生成 │ │
│ │ @MyMacro │ │ 生成代码 │ │ 验证正确 │ │ 最终产物 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ↑ │ │
│ │ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ 宏实现(Swift Package) │ │
│ │ - 解析语法树 │ │
│ │ - 生成新的语法节点 │ │
│ │ - 返回给编译器 │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
宏 vs 其他元编程方式
| 方式 | 时机 | 类型安全 | 可调试性 | 示例 |
|---|---|---|---|---|
| Swift 宏 | 编译时 | ✅ 完全 | ✅ 可展开查看 | @Observable |
| 泛型 | 编译时 | ✅ 完全 | ✅ | Array<T> |
| 协议扩展 | 编译时 | ✅ 完全 | ✅ | Collection.map() |
| 运行时反射 | 运行时 | ❌ 有限 | ❌ 困难 | Mirror |
| Sourcery | 构建时 | ❌ 外部工具 | ⚠️ 生成文件 | 代码生成 |
| C 宏 | 预处理 | ❌ 文本替换 | ❌ 难以调试 | #define |
宏的核心优势
// 1. 类型安全:宏生成的代码经过完整的类型检查
@Observable
class Counter {
var count = 0 // 宏确保正确实现 Observable 协议
}
// 2. 编译时验证:错误在编译时捕获
@MyMacro
struct Invalid {
// 如果结构不符合宏的要求,编译时报错
}
// 3. IDE 支持:可以展开查看生成的代码
// 在 Xcode 中右键 -> Expand Macro
@Observable // <- 右键可以看到展开后的完整代码
class Model {
var value = 0
}
// 4. 可组合:多个宏可以一起使用
@Observable
@MainActor
class ViewModel {
var data: [Item] = []
}
宏的类型
Swift 提供了 7 种宏角色(Macro Roles),每种适用于不同场景:
1. 独立表达式宏(Freestanding Expression Macro)
以 # 开头,生成一个表达式值。
// 定义
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(...)
// 使用
let (result, str) = #stringify(1 + 2)
// result = 3
// str = "1 + 2"
// 展开后等价于:
let (result, str) = (1 + 2, "1 + 2")
2. 独立声明宏(Freestanding Declaration Macro)
以 # 开头,生成一个或多个声明。
// 定义
@freestanding(declaration, names: named(CodingKeys))
public macro CodingKeys() = #externalMacro(...)
// 使用
struct User: Codable {
let firstName: String
let lastName: String
#CodingKeys() // 生成 CodingKeys 枚举
}
// 展开后:
struct User: Codable {
let firstName: String
let lastName: String
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
}
}
3. 附加成员宏(Attached Member Macro)
为类型添加新成员。
// 定义
@attached(member, names: named(id), named(createdAt))
public macro Identifiable() = #externalMacro(...)
// 使用
@Identifiable
struct Article {
var title: String
var content: String
}
// 展开后:
struct Article {
var title: String
var content: String
// 宏添加的成员
let id: UUID = UUID()
let createdAt: Date = Date()
}
4. 附加访问器宏(Attached Accessor Macro)
为属性添加 get/set 等访问器。
// 定义
@attached(accessor)
public macro UserDefault<T>(_ key: String, defaultValue: T) = #externalMacro(...)
// 使用
struct Settings {
@UserDefault("theme", defaultValue: "light")
var theme: String
}
// 展开后:
struct Settings {
var theme: String {
get {
UserDefaults.standard.string(forKey: "theme") ?? "light"
}
set {
UserDefaults.standard.set(newValue, forKey: "theme")
}
}
}
5. 附加成员属性宏(Attached MemberAttribute Macro)
为类型的所有成员添加属性。
// 定义
@attached(memberAttribute)
public macro PublishedProperties() = #externalMacro(...)
// 使用
@PublishedProperties
class ViewModel: ObservableObject {
var name: String = ""
var age: Int = 0
}
// 展开后:
class ViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
}
6. 附加对等宏(Attached Peer Macro)
在同级别添加新声明。
// 定义
@attached(peer, names: overloaded)
public macro AddAsync() = #externalMacro(...)
// 使用
struct API {
@AddAsync
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// 同步实现
}
}
// 展开后:
struct API {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// 同步实现
}
// 宏添加的异步版本
func fetchData() async throws -> Data {
try await withCheckedThrowingContinuation {
continuation in
fetchData {
result in
continuation.resume(with: result)
}
}
}
}
7. 附加扩展宏(Attached Extension Macro)
为类型添加扩展。
// 定义
@attached(extension, conformances: Equatable, Hashable, names: named(==), named(hash))
public macro AutoEquatable() = #externalMacro(...)
// 使用
@AutoEquatable
struct Point {
var x: Double
var y: Double
}
// 展开后:
struct Point {
var x: Double
var y: Double
}
extension Point: Equatable, Hashable {
static func == (lhs: Point, rhs: Point) -> Bool {
lhs.x == rhs.x && lhs.y == rhs.y
}
func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
宏角色组合
一个宏可以同时具有多个角色:
// @Observable 就是一个多角色宏
@attached(member, names: ...)
@attached(memberAttribute)
@attached(extension, conformances: Observable)
public macro Observable() = #externalMacro(...)
内置宏
Swift 标准库内置宏
1. #warning 和 #error
// 编译时警告
#warning("TODO: Optimize this algorithm")
// 编译时错误
#if DEBUG
let apiKey = "debug-key"
#else
#error("Production API key not configured")
#endif
// 条件警告
func deprecatedFunction() {
#warning("This function will be removed in v2.0")
}
2. #file, #line, #function, #column
func log(
_ message: String,
file: String = #file,
line: Int = #line,
column: Int = #column,
function: String = #function
) {
print("[\(file):\(line):\(column)] \(function): \(message)")
}
// 调用
log("Something happened")
// 输出: [/path/to/File.swift:42:5] myFunction(): Something happened
// Swift 5.8+ 新增
#fileID // "ModuleName/FileName.swift"
#filePath // 完整路径(调试用)
3. #selector 和 #keyPath
class MyClass: NSObject {
@objc var name: String = ""
@objc func handleTap()
浙公网安备 33010602011771号