Swift Optional Chaining

时间:2022-12-04 12:41:08

Optional Chaining介绍

关于「optional chaining」,《The Swift Programming Language》是这么描述的:

Optional chaining is a process for querying and calling properties, methods, and subscripts on an optional that might currently be nil. If the optional contains a value, the property, method, or subscript call succeeds; if the optional is nil, the property, method, or subscript call returns nil. Multiple queries can be chained together, and the entire chain fails gracefully if any link in the chain is nil.

P.S:「Optional Chaining」有各种各样的翻译,譬如「自判断链接」「可选链表」等,个人觉得「可选链表」还凑合,本文尽量使用英文表述。

P.S:Swift中的「optional chaining」有些类似于OC中「向nil发送消息」,简而言之,在OC中,向一个对象(指针)发送消息时,若这个指针为nil,则啥都不干;Swift的「optional chaining」有类似的效果;只不过OC这种处理只适用于类对象,Swift的「optional chaining」适用于任意类型。此外,在OC中,我们想一个可能为nil的对象发送一个消息时,通常我们不知道这个消息是否被执行了(尤其是该消息么有返回值时),但在Swift中,可以通过检查返回值来判断实例的方法是否被调用,后文会详细阐述。

可选链表 v.s 强制解包

在Swift中,访问某些属性/方法/下标得到的经果常常是一个optional,在这些返回值为optional的属性/方法/下标后放上一个问号?就构成了所谓的「optional chaining」。
这有一些类似于强制解包(在可选类型后面放上一个感叹号!强制解包)。他们的在于当optional为nil时「optional chaining」即刻失败,然而一般的强制解包操作会引发runtime错误。

下面这段话非常重要。
因为「optional chaining」是随时否可以提前返回nil的,所以使用optional chaining所得到的东西都是optional,even if the property, method, or subscript you are querying returns a non-optional value. 下面以代码来演示:

class Toy {
let name: String
init(name: String) {
self.name = name
}
} class Pet {
var toy: Toy?
} class Child {
var pet: Pet?
}

在实际使用过程中,我们想要知道小明(名为xiaoming的Child对象)的宠物的玩具的名字的时候,可以通过下面的「optional chaining」查询:

let toyName = xiaoming.pet?.toy?.name

注意,我们最后访问的是name,并且在Toy的定义中name是被定义为一个确定的String而非String?的,但是我们拿到的toyName起始还是一个String?的类型。这是由于在「optional chaining」中我们在任意一个?.的时候都可能遇到nil而提前返回,这个时候当然只能获取到nil了。

所以,在实际使用中,我们大多数情况下可能更希望使用「Optional Binding」来直接取值,如下:

if let toyName = xiaoming.pet?.toy?.name {
// 巴拉巴拉
}

总之,使用「optional chaining」的返回结果一定是一个optional。

OK,现在以几段代码来解释「可选链表」(即?.)和「强制解包」(即!.)的不同。

首先定义两个类Person和Residence,如下:

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

Residence具有一个Int类型属性numberOfRooms,其值为1。Person具有一个optional属性residence,它的类型是Residence?

如果你创建一个新的Person实例,它的residence属性由于是被定义为自判断型的,此属性将默认初始化为空:

let john = Person()

如果你想使用声明符!强制解包获得这个人residence属性的numberOfRooms属性值,将会引发运行时错误,因为这时没有可以供拆包的residence值,如下:

let roomCount = john.residence!.numberOfRooms
// 将导致运行时错误

当john.residence不是nil时,会正常运行,且会将roomCount设置为一个Int类型的合理值。然而,如上所述,当residence为空时,这个代码将会导致运行时错误。

自判断链接提供了一种另一种获得numberOfRooms的方法。利用自判断链接,使用问号来代替原来!的位置:

if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
} /* 输出:
Unable to retrieve the number of rooms.
*/

通过Optional Chaining访问属性

正如上文可选链表 v.s 强制解包所述,你可以利用「optional chaining」获取属性,并且检查属性是否成功。

使用上述定义的类来创建一个人实例,并再次尝试后去它的numberOfRooms属性:

let john = Person()
if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
}
/*输出:
Unable to retrieve the number of rooms.
*/

由于john.residence是空,所以这个自判断链接和之前一样失败了,但是没有运行时错误。

你还可以使用「optional chaining」来设置属性值,如下:

john.residence?.numberOfRooms = 

通过Optional Chaining调用方法

你可以使用「optional chaining」的调用某个optional的方法,并可以检查方法调用是否成功,哪怕这个方法没有返回值。

在上文的Residence中添加一个实例方法printNumberOfRooms,该方法会打印numberOfRooms的当前值。方法如下:

func printNumberOfRooms(){
println("The number of rooms is \(numberOfRooms)")
}

这个方法没有返回值。但是,在Swift中,若函数和方法没有显式提供返回值,则Swift会为它们提供一个隐式的返回值类型Void。如果你利用自判断链接调用此方法,这个方法的返回值类型将是Void?,而不是Void,因为当通过「optional chaining」调用方法时返回值总是optional,即使是这个方法本是没有定义返回值,你也可以使用if语句来检查是否能成功调用printNumberOfRooms方法:如果方法通过自判断链接调用成功,printNumberOfRooms的隐式返回值将会是Void,如果没有成功,将返回nil:

if john.residence?.printNumberOfRooms() != nil {
println("It was possible to print the number of rooms.")
} else {
println("It was not possible to print the number of rooms.")
}
/*输出:
It was not possible to print the number of rooms.
*/

通过Optional Chaining访问下标

还可以在「optional chaining」中通过「下标」(subscripts)获取和设置一个optional,同样可以检查是否获取成功;

通过「Optional Chaining」访问下标时,一定要注意:

When you access a subscript on an optional value through optional chaining, you place the question mark (?before the subscript’s braces ([]), not after. The optional chaining question mark always follows immediately after the part of the expression that is optional.

举个栗子,在上文Person和Residence的基础上添加一个新的类Room:

class Room {
let name: String
init(name: String) {
self.name = name
}
}

在Residence类中添加一个subscript和一个数组属性rooms,如下:

class Residence {
...
var rooms = [Room]()
subscript(i: Int) -> Room {
return rooms[i]
}
...
}

通过「Optional Chaining」访问下标如下:

if let firstRoomName = john.residence?[].name {
println("The first room name is \(firstRoomName).")
} else {
println("Unable to retrieve the first room name.")
} /*输出:
Unable to retrieve the first room name.
*/

同样,除了获取,还可以设置:

john.residence?[] = Room(name: "Bashroom")