Embrace Swift type inference
Swift & UI 进阶 26m

拥抱 Swift 类型推断

Embrace Swift type inference

2020年6月25日

在 Apple 官方观看视频

一句话判断

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 还讨论了什么时候应该显式标注类型——当类型推断的结果不够直观、当编译器报错信息不够友好、或者当你需要利用类型标注来引导编译器选择特定的重载时。这不是”全有或全无”的选择,而是”让编译器做它擅长的事,在必要的地方给提示”。

值得深挖的点

闭包推断与尾随闭包的交互

闭包是类型推断发挥最大作用的场景之一。当你使用 mapfilter 等高阶函数时,闭包参数的类型和返回值类型都可以从上下文推断出来。比如 [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)
    }
}

最佳实践

  • public API 的函数签名中始终标注参数和返回值类型,这是模块间的”契约”
  • 私有和内部实现中尽量省略类型标注,让代码更简洁
  • 当编译器的错误信息让你困惑时,给中间变量加上类型标注帮助定位问题——调试完后再移除
  • 在复杂的泛型链中,在关键节点添加类型标注可以帮助编译器更快地完成类型检查
  • 使用 some 关键字(opaque return type)替代泛型参数,既保持了推断的灵活性又隐藏了实现细节

还有什么值得关注

  • Session 提到 Swift 编译器在 Xcode 12 中的类型检查速度有明显提升,特别是对于包含大量字符串字面量的代码
  • 类型推断与 async/await 的配合是未来方向——异步代码的返回类型推断将更加智能
  • 多尾随闭包语法(Swift 5.3)让闭包的类型推断更加自然,不再需要用 $0$1 引用匿名参数
WWDC 2020