拥抱 Swift 类型推断
Embrace Swift type inference
2020年6月25日
一句话判断
Swift 的类型推断不是偷懒——它是让你写出更简洁、更安全的代码的核心能力。如果你还在每个变量前显式标注类型,你可能还没真正理解 Swift 的设计哲学。
这场 Session 讲了什么
这场 Session 是一次关于 Swift 类型推断系统的深入讲解。Apple 的工程师从编译器视角解释了类型推断的工作原理,然后通过大量代码示例展示了如何”信任”类型推断来写出更地道的 Swift 代码。核心论点是:显式类型标注不是”更安全”,在大多数情况下反而是噪音——真正的安全性来自编译器的完整推断链条。
Session 分为三个层次。基础部分展示了日常代码中可以安全省略的类型标注,比如 let x = 5 不需要写 let x: Int = 5。进阶部分讨论了泛型函数和闭包中的类型推断,以及如何通过上下文信息让编译器推断出正确的类型。高级部分涉及了 Result Builder、Property Wrapper 等 Swift 5.3/5.4 特性中类型推断的关键角色。
Session 还讨论了什么时候应该显式标注类型——当类型推断的结果不够直观、当编译器报错信息不够友好、或者当你需要利用类型标注来引导编译器选择特定的重载时。这不是”全有或全无”的选择,而是”让编译器做它擅长的事,在必要的地方给提示”。
值得深挖的点
闭包推断与尾随闭包的交互
闭包是类型推断发挥最大作用的场景之一。当你使用 map、filter 等高阶函数时,闭包参数的类型和返回值类型都可以从上下文推断出来。比如 [1,2,3].map { $0 * 2 } 不需要任何类型标注。Session 指出,Swift 5.3 中多个尾随闭包的语法改进进一步增强了这种能力——你可以在更自然的语法位置提供闭包,而类型由上下文自动确定。
类型推断与编译时间的权衡
Session 坦诚地讨论了类型推断对编译时间的影响。在极端情况下,复杂的泛型嵌套和过度使用字面量推断确实会增加编译时间。Apple 的建议是:在模块的公共 API(public 函数签名)中始终提供显式类型标注,这是对使用者的一份”类型契约”;在内部实现中则大胆使用推断,让代码更简洁。
代码片段
// 基础:让编译器推断类型
// 冗余写法
let name: String = "张三"
let age: Int = 25
let prices: [Double] = [9.9, 19.9, 29.9]
let scores: Dictionary<String, Int> = ["语文": 90, "数学": 95]
// 地道写法:信任类型推断
let name = "张三" // 推断为 String
let age = 25 // 推断为 Int
let prices = [9.9, 19.9, 29.9] // 推断为 [Double]
let scores = ["语文": 90, "数学": 95] // 推断为 [String: Int]
// 函数返回值推断
func makeGreeting(for name: String) -> some StringProtocol {
// 返回类型由表达式推断,调用方只关心它是 StringProtocol
"你好,\(name)"
}
// 进阶:泛型和闭包中的类型推断
struct DataLoader<T: Decodable> {
let decode: (Data) throws -> T
// 工厂方法利用类型推断简化创建
static func jsonDecoder() -> DataLoader where T: Decodable {
DataLoader { data in
// T 的类型从外部上下文推断
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}
}
// 使用时不需要指定泛型参数
let userLoader = DataLoader<User>.jsonDecoder()
// 等价于显式写法,但更简洁
// 高阶函数中的推断
let users = [
User(name: "Alice", age: 28),
User(name: "Bob", age: 35),
User(name: "Charlie", age: 22),
]
// 闭包参数类型、返回值类型全部推断
let names = users.map { $0.name } // 推断为 [String]
let adults = users.filter { $0.age >= 18 } // 推断为 [User]
let totalAge = users.reduce(0) { $0 + $1.age } // 推断为 Int
// 高级:Property Wrapper 和 Result Builder 中的推断
// Property Wrapper 利用泛型推断存储值的类型
@propertyWrapper
struct Cached<T> {
private var value: T?
private let loader: () -> T
private let expiration: TimeInterval
var wrappedValue: T {
mutating get {
if value == nil {
value = loader()
}
return value!
}
}
init(expiration: TimeInterval = 300, loader: @escaping () -> T) {
self.expiration = expiration
self.loader = loader
}
}
// 使用时类型完全推断
struct ViewModel {
// T 推断为 [String],不需要标注
@Cached(expiration: 60) var recentItems = {
return loadFromDisk() ?? []
}()
}
// some 关键字与推断配合
func makeView() -> some View {
// SwiftUI 视图的具体类型由 body 推断
VStack {
Text("Hello")
.font(.title)
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
最佳实践
- 在
publicAPI 的函数签名中始终标注参数和返回值类型,这是模块间的”契约” - 私有和内部实现中尽量省略类型标注,让代码更简洁
- 当编译器的错误信息让你困惑时,给中间变量加上类型标注帮助定位问题——调试完后再移除
- 在复杂的泛型链中,在关键节点添加类型标注可以帮助编译器更快地完成类型检查
- 使用
some关键字(opaque return type)替代泛型参数,既保持了推断的灵活性又隐藏了实现细节
还有什么值得关注
- Session 提到 Swift 编译器在 Xcode 12 中的类型检查速度有明显提升,特别是对于包含大量字符串字面量的代码
- 类型推断与
async/await的配合是未来方向——异步代码的返回类型推断将更加智能 - 多尾随闭包语法(Swift 5.3)让闭包的类型推断更加自然,不再需要用
$0、$1引用匿名参数