17.1. 使用可选链式调用代替强制解包

通过在想调用的属性、方法,或下标的可选值后面放一个问号(?),可以定义一个可选链。这一点很像在可选值后面放一个叹号(!)来强制解包它的值。它们的主要区别在于当可选值为空时可选链式调用只会调用失败,然而强制解包将会触发运行时错误。

为了反映可选链式调用可以在空值(nil)上调用的事实,不论这个调用的属性、方法及下标返回的值是不是可选值,它的返回结果都是一个可选值。你可以利用这个返回值来判断你的可选链式调用是否调用成功,如果调用有返回值则说明调用成功,返回 nil 则说明调用失败。

这里需要特别指出,可选链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选值。例如,使用可选链式调用访问属性,当可选链式调用成功时,如果属性原本的返回结果是 Int 类型,则会变为 Int? 类型。

下面几段代码将解释可选链式调用和强制解包的不同。

首先定义两个类 PersonResidence

class Person {
    var residence: Residence?
}
class Residence {
    var numberOfRooms = 1
}

Residence 有一个 Int 类型的属性 numberOfRooms,其默认值为 1Person 具有一个可选的 residence 属性,其类型为 Residence?

假如你创建了一个新的 Person 实例,它的 residence 属性由于是可选类型而将被初始化为 nil,在下面的代码中,john 有一个值为 nilresidence 属性:

let john = Person()

如果使用叹号(!)强制解包获得这个 johnresidence 属性中的 numberOfRooms 值,会触发运行时错误,因为这时 residence 没有可以解包的值:

let roomCount = john.residence!.numberOfRooms
// 这会引发运行时错误

john.residence 为非 nil 值的时候,上面的调用会成功,并且把 roomCount 设置为 Int 类型的房间数量。正如上面提到的,当 residencenil 的时候,上面这段代码会触发运行时错误。

可选链式调用提供了另一种访问 numberOfRooms 的方式,使用问号(?)来替代原来的叹号(!):

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// 打印“Unable to retrieve the number of rooms.”

residence 后面添加问号之后,Swift 就会在 residence 不为 nil 的情况下访问 numberOfRooms

因为访问 numberOfRooms 有可能失败,可选链式调用会返回 Int? 类型,或称为“可选的 Int”。如上例所示,当 residencenil 的时候,可选的 Int 将会为 nil,表明无法访问 numberOfRooms。访问成功时,可选的 Int 值会通过可选绑定解包,并赋值给非可选类型的 roomCount 常量。

要注意的是,即使 numberOfRooms 是非可选的 Int 时,这一点也成立。只要使用可选链式调用就意味着 numberOfRooms 会返回一个 Int? 而不是 Int

可以将一个 Residence 的实例赋给 john.residence,这样它就不再是 nil 了:

john.residence = Residence()

john.residence 现在包含一个实际的 Residence 实例,而不再是 nil。如果你试图使用先前的可选链式调用访问 numberOfRooms,它现在将返回值为 1Int? 类型的值:

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// 打印“John's residence has 1 room(s).”