既然我们已经知道值存在,为什么 Swift 中还需要隐式解包可选类型?
你是否曾经在阅读他人的 Swift 代码时,看到变量声明中带有感叹号而感到困惑?var name: String! — 这不仅仅是一个可选类型,而是隐式解包可选类型(IUO)。这时一个合理的问题出现了:“如果你确定值存在,为什么要创建这样的东西?为什么不直接使用普通的 String?”
这个问题困扰着成千上万的开发者,在 Stack Overflow 上它已经获得了数万次浏览。今天我们将一劳永逸地弄清楚为什么需要 IUO,何时使用它们,以及何时最好远离它们。开始吧!
可选类型快速回顾(针对那些已经忘记的人)
在深入 IUO 之前,我们先回顾一下什么是普通的可选类型。在 Swift 中,一个可能为 nil 的变量必须声明为可选类型:
var maybeNumber: Int? = 42maybeNumber = nil // 这是正常的要使用该值,需要解包(unwrap)——要么通过 if let、guard let,要么通过强制解包(force unwrap):
print(maybeNumber!) // 危险!如果为 nil 则会崩溃现在想象一下,你绝对确定变量在访问时永远不会是 nil,但由于某种原因,你无法在初始化时为其赋值。这时隐式解包可选类型就出现了。
什么是隐式解包可选类型?
隐式解包可选类型是一种可选类型,在访问时会自动解包。你使用 ! 而不是 ? 来声明它:
var name: String! = "Alice"print(name) // "Alice" — 自动解包了!请注意:我们不需要写 name!。系统会自动为我们做这件事。然而,如果在访问时 name 是 nil,应用程序将会崩溃并报错——就像强制解包一样。
IUO 是对编译器的一个承诺:“我知道我在做什么。这个值会存在,即使现在还没有。” 这是在可选类型的安全性和普通类型的便利性之间的一种折衷。
IUO 的主要使用场景
1. Interface Builder 中的 Outlets(UIKit)
最常见的例子是 iOS 开发中的 @IBOutlet。当你在代码和 Storyboard 之间建立连接时,outlet 无法在 init 中初始化,因为视图尚未加载。但在加载后(viewDidLoad),你确切知道 outlet 存在。以前(在 Swift 3 之前),所有 outlets 都声明为 IUO:
@IBOutlet var label: UILabel!为什么不用 UILabel??因为那样每次访问 label 时都必须写 label! 或使用 guard let。这很繁琐且使代码变得杂乱。IUO 使代码更简洁,同时保持安全性(如果你不小心忘记连接 outlet,崩溃会立即发生,你可以快速找到错误)。
重要提示: 从 Xcode 9 和 Swift 4 开始,Apple 建议对 outlets 使用普通的可选类型 UILabel?。但许多项目仍然使用 IUO,并且这仍然有效。
2. 两阶段初始化(Two-Phase Initialization)
有时你无法在初始化时为属性赋值,但保证在首次使用之前它会被设置。一个典型的例子是在调用另一个方法后设置的依赖项:
class NetworkManager { var apiKey: String! func configure(with key: String) { self.apiKey = key } func fetchData() { // 这里 apiKey 肯定不是 nil print("Fetching