新 IUO 详解

今年 WWDC 上提了一下 Swift 废除 IUO 类型的事情,给了我一个机会去更加深入了解 IUO 相关的内容,实际上 IUO 的行为在 Swift 3 里就增强限制了,为了这一次的修改作了铺垫,这次在 Swift 4.2 里终于彻底去掉了 IUO 类型。

我在写这篇 What’s New in Swift 的时候找了很多资料,但找不到一篇文章把这件事情说的很清楚,一方面是社区对于这件事情的关心也不是很大,另一方面是这个修改在 Swift 3 里已经做了铺垫,对于现有代码的破坏性很小。而且官方提案里也是写得很含糊,写得最好的是,虽然前面那篇 WWDC 的专题文章已经讲过了大概的概念,但我后来看了别人的一些文章,自己也思考了一段时间之后,觉得还是可以单独拎出来写篇文章,用更简单易懂的方式来讲清楚这个修改。

IUO 的概念

在开始讲之前,我们需要补充一些背景知识,什么是 IUO?

Optional 是现代编程语言都乐于拥有的一个功能,用来显式地标记变量的 nullability —— 是否能为 null,加强了代码的自文档能力,获取 Optional 值的时候,我们需要先把内部的值解包出来才能使用,由此大大减少了空值导致的问题:

func foo(bar: Int?) {
guard let value = bar else { return }
doSomething(value)
}

Optional 虽然安全,但是有一些变量,它们的初始值是 nil,但是在某个生命周期之后就会有值,例如说 UIViewController 里的 view,如果每一次使用都进行手动解包的话,那这些解包的代码就很容易淹没掉代码整体的意图。

由此就诞生了 IUO,IUO 全名是 ImplicityUnwrappedOptional,翻译过来就是隐式(自动)解包的可选值,让我们可以在使用 Optional 值时自动解包,但如果解包失败的话就会导致程序崩溃:

let x: Int! = 0
let y: Int! = nil

print(x + 2) // x 被自动解包
print(y + 2) // y 解包失败,程序崩溃

新旧 IUO 的区别

旧的 IUO 是一个类型,而新的 IUO 是一个变量标记。

新的 IUO 是一个变量标记,但它只能标记 Optional 类型的变量,IUO 标记本质上是 @_autounwrapped 标记:

let x: Int! = 0
// 会被预编译为
@_autounwrapped let x: Int? = 0

而所有被标记为 @_autounwrapped 的变量,都会在特定的情况下被自动解包:

let y = x + 1
// 会被预编译为
let y = x! + 1

改进的原因

使用 IUO 是为了标记变量,告诉编译器,一旦使用了这种被标记的变量,就自动解包。而 Swift 4.2 以前,IUO 是作为一个类型 ImplicityUnwrappedOptional 存在的,会有一些不好的副作用产生。

类型会被传递

类型是会被传递的,因为类型实际上标记的不只是变量,它标记的是,而值是可传递的,例如:

let x: Int! = nil
let y = x // y 的类型为 Int!

我们把值传递给 y 的时候传递的是 ImplicityUnwrappedOptional<Int>.none 这个值,导致 y 的类型页被推导为 Int!,然而我们想要标记的其实只是 x 这个变量,这就偏离了最初的目的。

并且这样的代码会让 IUO 肆意扩散,由于 IUO 的特性,甚至会让代码的使用者意识不到他正在使用的变量可能为空,例如一次变量声明的修改:

func foo() -> Int { return 1 }

let x = foo()
print(x + 1)

// 此时如果修改函数 foo 的返回值以及实现,依旧可以正常编译
func foo() -> Int! { return nil }

我们依赖的函数或者变量,把返回值或者变量的类型修改为 IUO 之后,实际上还是可以让我们的代码正常通过编译,但作为使用者的我们对此一无所知,这很容易造成程序崩溃。

而 Swift 4.2 里,因为 @_autounwrapped 这个标记是绑定在变量上,而不是绑定在值上面的,所以不会被传递出去,而且 Int! 本质上是 Int? 类型:

// Swift 4.2 以前
let x: Int! = nil
let y = x // y 的类型为 Int!

// Swift 4.2
let x: Int! = nil
let y = x // y 的类型为 Int?

IUO 作为类型时,可空性不明确

在看一些 Swift 新手写代码的时候,偶尔会看到他们使用 IUO 作为函数参数或者返回值:

func foo(bar: Int!) -> Int! { ... }

这种写法很不好,bar 声明的类型有三种选择:

  1. Intbar 不可能为空,使用 Int 就是最合适的选择,并且借助 Swift 强大的编译器,可以保证调用方传入的绝对是非空值。
  2. Int?bar 可能为空,使用 Int? 就强迫使用方在解包之后才能正常使用值,并且可以考虑到空值情况的处理
  3. Int!:???

使用 Int! 是最不推荐的作法,因为传入的参数只有两种可能性,要么可能为空,要么不可能为空,而 IUO 这种模凌两可的声明不只会让调用方感到迷惑,而且还很容易在实现时产生纰漏:

func foo(bar: Int!) -> Int! {
print(bar + 1) // 当 bar 为 nil 时就会 crash
guard let bar = bar else { return nil }
return bar += 2
}

如果函数的参数比较多,而且逻辑比较复杂的时候,这种方式会让新手可能在解包前就访问了变量,让老手在使用变量时需要一直考虑是否已经了判断空值,无论哪种情况都显得很糟糕。

虽然目前出于 API 兼容性的原因还是可以把传入参数和返回值声明为 IUO,但我个人觉得还是不应该存在这种行为。

总结

写完才想起了 swift.org 这篇 Reimplementation of Implicitly Unwrapped Optionals,如果还对 IUO 存在疑惑的话可以考虑看看这一篇。