翻译自:https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html
可选链是一个在当前可能为nil
的可选链上查询和调用属性、方法和下标的过程。如果可选包含值,则属性、方法或下标调用成功;如果可选值为nil
,则属性、方法或下标调用返回nil
。多个查询可以链接在一起,如果链中的任何链接为nil
整个链条会优雅地失败。
注意
Swift中的可选链类似于Objective-C中的消息nil
,但以适用于任何类型的方式,并且可以检查成功或失败。
可选链作为强制打开包装的替代方案
您通过放置问号(?
)来指定可选的链条在可选值之后,如果可选值为非nil
在您希望调用属性、方法或下标的可选值之后。这与放置感叹号(!
)非常相似在可选值之后,强制展开其值。主要区别在于,当可选链接为nil
,可选链会优雅地失败,而当可选链接为nil
,强制展开包装会触发运行时错误。
为了反映可选链可以在nil
值上调用的事实,可选链调用的结果始终是可选值,即使您正在查询的属性、方法或下标返回非可选值。您可以使用此可选返回值来检查可选链调用是否成功(返回的可选包含值),还是由于链中的nil
值而没有成功(返回的可选值为nil
)。
具体来说,可选链调用的结果与预期返回值类型相同,但包装在可选中。通常返回Int
的属性会返回Int?
通过可选链访问时。
接下来的几个代码片段演示了可选链与强制拆开的区别,并使您能够检查成功。
First, two classes called Person
and Residence
are defined:
- class Person {
- var residence: Residence?
- }
- class Residence {
- var numberOfRooms = 1
- }
Residence
实例有一个名为numberOfRooms``Int
属性,默认值为1。Person
实例具有Residence?
类型为可选的residence
属性。
如果您创建一个新的Person
实例,其residence
属性默认初始化为nil
,因为它是可选的。在下面的代码中,john
的residence
物业价值为nil
:
- let john = Person()
如果您尝试访问此人residence
的numberOfRooms
属性,通过在residence
后放置感叹号以强制解开其值,您将触发运行时错误,因为没有residence
值可以打开:
- let roomCount = john.residence!.numberOfRooms
- // this triggers a runtime error
john.residence
具有非nil
值时,上述代码将成功,并将roomCount
设置为包含适当数量房间的Int
值。然而,如上所述,当residence
为nil
,此代码总是触发运行时错误。
可选链提供了一种访问numberOfRooms
值的替代方式。要使用可选的链条,请使用问号代替感叹号:
- if let roomCount = john.residence?.numberOfRooms {
- print(“John’s residence has (roomCount) room(s).”)
- } else {
- print(“Unable to retrieve the number of rooms.”)
- }
- // Prints “Unable to retrieve the number of rooms.”
这告诉Swift在可选的residence
属性上“链”,如果存在residence
,则检索numberOfRooms
值。
由于访问numberOfRooms
尝试可能会失败,因此可选的链式尝试返回类型为Int?
或“optional Int
”的值。如上例所示,当residence
为nil
,此可选的Int
也将为nil
,以反映无法访问numberOfRooms
的事实。可选的Int
通过可选绑定访问,以解开整数,并将非可选值分配给roomCount
常量。
请注意,即使numberOfRooms
是一个非可选的Int
也是如此。它通过可选链查询的事实意味着对numberOfRooms
调用将始终返回anIntInt?
而不是Int
。
您可以将Residence
实例分配给john.residence
,使其不再具有nil
值:
- john.residence = Residence()
john.residence
现在包含一个实际的Residence
实例,而不是nil
。如果您尝试使用与以前相同的可选链条访问numberOfRooms
,它现在将返回一个Int?
包含默认numberOfRooms
值为1:
- if let roomCount = john.residence?.numberOfRooms {
- print(“John’s residence has (roomCount) room(s).”)
- } else {
- print(“Unable to retrieve the number of rooms.”)
- }
- // Prints “John’s residence has 1 room(s).”
定义可选链的模型类
您可以使用可选链来调用多个级别深的属性、方法和下标。这使您能够深入了解相互关联的复杂模型中的子属性,并检查是否可以访问这些子属性的属性、方法和下标。
下面的代码片段定义了四个模型类,用于后续几个示例,包括多级可选链的示例。这些类通过添加Room
和Address
类以及相关的属性、方法和下标来扩展上面的Person``Residence
模式。
Person
类的定义与以前相同:
- class Person {
- var residence: Residence?
- }
Residence
舱比以前更复杂。这一次,Residence
类定义了一个名为rooms
的变量属性,该属性使用[Room]
类型的空数组初始化:
- class Residence {
- var rooms: [Room] = []
- var numberOfRooms: Int {
- return rooms.count
- }
- subscript(i: Int) -> Room {
- get {
- return rooms[i]
- }
- set {
- rooms[i] = newValue
- }
- }
- func printNumberOfRooms() {
- print(“The number of rooms is (numberOfRooms)”)
- }
- var address: Address?
- }
由于此版本的Residence
存储了一个Room
实例数组,因此其numberOfRooms
属性作为计算属性实现,而不是存储属性。computednumberOfRooms属性只需从rooms
数组返回count
属性的值。
作为访问其rooms
数组的快捷方式,此版本的Residence
提供了一个读写下标,该下标可根据rooms
数组中请求的索引访问房间。
这个版本的Residence
还提供了一种名为printNumberOfRooms
的方法,它只需打印住宅中的房间数量。
Finally, Residence
defines an optional property called address
, with a type of Address?
. The Address
class type for this property is defined below.
用于rooms
数组的Room
类是一个简单的类,有一个名为name
的属性,以及将该属性设置为合适房间名称的初始化器:
- class Room {
- let name: String
- init(name: String) { self.name = name }
- }
这个模型中的最后一个类称为Address
。该类有三个typeStringString?
的可选属性。前两个属性,buildingName
和buildingNumber
,是将特定建筑物识别为地址一部分的替代方法。第三种财产,street
,用于为该地址命名街道:
- class Address {
- var buildingName: String?
- var buildingNumber: String?
- var street: String?
- func buildingIdentifier() -> String? {
- if let buildingNumber = buildingNumber, let street = street {
- return “(buildingNumber) (street)”
- } else if buildingName != nil {
- return buildingName
- } else {
- return nil
- }
- }
- }
The Address
class also provides a method called buildingIdentifier()
, which has a return type of String?
. This method checks the properties of the address and returns buildingName
if it has a value, or buildingNumber
concatenated with street
if both have values, or nil
otherwise.
通过可选链访问属性
正如可选链作为强制打开包装的替代方案所示,您可以使用可选链访问可选值上的属性,并检查该属性访问是否成功。
使用上面定义的类创建一个新的Person
实例,并尝试像以前一样访问其numberOfRooms
属性:
- let john = Person()
- if let roomCount = john.residence?.numberOfRooms {
- print(“John’s residence has (roomCount) room(s).”)
- } else {
- print(“Unable to retrieve the number of rooms.”)
- }
- // Prints “Unable to retrieve the number of rooms.”
由于john.residence
为nil
,这个可选的链式调用与以前一样失败。
您还可以尝试通过可选链设置属性的值:
- let someAddress = Address()
- someAddress.buildingNumber = “29”
- someAddress.street = “Acacia Road”
- john.residence?.address = someAddress
在本例中,设置john.residence``address
属性的尝试将失败,因为john.residence
目前为nil
。
该赋值是可选链的一部分,这意味着没有计算=
运算符右侧的代码。在上一个示例中,不容易看到someAddress
从未被评估过,因为访问常量没有任何副作用。以下列表执行相同的分配,但它使用函数来创建地址。该函数在返回值之前打印“函数已调用”,该值允许您查看是否计算了=
运算符的右侧。
- func createAddress() -> Address {
- print(“Function was called.”)
- let someAddress = Address()
- someAddress.buildingNumber = “29”
- someAddress.street = “Acacia Road”
- return someAddress
- }
- john.residence?.address = createAddress()
您可以判断没有调用createAddress()
函数,因为没有打印任何东西。
通过可选链调用方法
您可以使用可选链调用可选值上的方法,并检查该方法调用是否成功。即使该方法没有定义返回值,您也可以这样做。
Residence
类上的printNumberOfRooms()
方法打印numberOfRooms
当前值。以下是方法的外观:
- func printNumberOfRooms() {
- print(“The number of rooms is (numberOfRooms)”)
- }
此方法没有指定返回类型。然而,没有返回类型的函数和方法具有隐式返回类型为Void
,如《没有返回值的函数》中所述。这意味着它们返回一个值()
或一个空元组。
If you call this method on an optional value with optional chaining, the method’s return type will be Void?
, not Void
, because return values are always of an optional type when called through optional chaining. This enables you to use an if
statement to check whether it was possible to call the printNumberOfRooms()
method, even though the method doesn’t itself define a return value. Compare the return value from the printNumberOfRooms
call against nil
to see if the method call was successful:
- if john.residence?.printNumberOfRooms() != nil {
- print(“It was possible to print the number of rooms.”)
- } else {
- print(“It was not possible to print the number of rooms.”)
- }
- // Prints “It was not possible to print the number of rooms.”
如果您尝试通过可选链设置属性,也是如此。上面通过可选链访问属性中的示例试图为john.residence
设置address
值,即使residence
属性为nil
。任何通过可选链设置属性的尝试都会返回Void?
类型的值,这使您能够与nil
进行比较,看看属性是否已成功设置:
- if (john.residence?.address = someAddress) != nil {
- print(“It was possible to set the address.”)
- } else {
- print(“It was not possible to set the address.”)
- }
- // Prints “It was not possible to set the address.”
通过可选链条访问下标
您可以使用可选链尝试从可选值的下标中检索和设置值,并检查该下标调用是否成功。
注意
当您通过可选链访问可选值的下标时,您将问号放在下标括号之前,而不是之后。可选的链式问号总是紧随其后于表达式的可选部分之后。
下面的示例试图使用Residence
类上定义的下标检索john.residence
属性的rooms
数组中第一个房间的名称。由于john.residence
目前为nil
,下标调用失败:
- if let firstRoomName = john.residence?[0].name {
- print(“The first room name is (firstRoomName).”)
- } else {
- print(“Unable to retrieve the first room name.”)
- }
- // Prints “Unable to retrieve the first room name.”
此下标调用中的可选链问号立即放在john.residence
之后的下标括号之前,因为john.residence
是尝试可选链的可选值。
同样,您可以尝试通过带有可选链的下标设置新值:
- john.residence?[0] = Room(name: “Bathroom”)
此下标设置尝试也失败,因为residence
目前为nil
。
如果您创建并向john.residence
分配实际的Residence
实例,其rooms
数组中有一个或多个Room
实例,您可以使用Residence
下标通过可选链访问rooms
数组中的实际项目:
- let johnsHouse = Residence()
- johnsHouse.rooms.append(Room(name: “Living Room”))
- johnsHouse.rooms.append(Room(name: “Kitchen”))
- john.residence = johnsHouse
- if let firstRoomName = john.residence?[0].name {
- print(“The first room name is (firstRoomName).”)
- } else {
- print(“Unable to retrieve the first room name.”)
- }
- // Prints “The first room name is Living Room.”
访问可选类型的下标
如果下标返回可选类型的值(例如Swift’sDictionary类型的键下标),请在下标的闭括号后放置一个问号,以链式链接到其可选返回值:
- var testScores = [“Dave”: [86, 82, 84], “Bev”: [79, 94, 81]]
- testScores[“Dave”]?[0] = 91
- testScores[“Bev”]?[0] = 1
- testScores[“Brian”]?[0] = 72
- // the “Dave” array is now [91, 82, 84] and the “Bev” array is now [80, 94, 81]
上面的示例定义了一个名为testScores
字典,其中包含两个键值对,将String
键映射到Int
值数组。该示例使用可选链将"Dave"
数组中的第一个项目设置为91
;将"Bev"
数组中的第一个项目增加1
;并尝试将数组中的第一个项目设置为"Brian"
的键。前两个调用成功了,因为testScores
字典包含"Dave"
和"Bev"
的键。第三次调用失败,因为testScores
字典不包含"Brian"
的密钥。
连接多个级别的链条
您可以将多个级别的可选链链接在一起,以深入了解模型中更深处的属性、方法和下标。然而,多个级别的可选链不会为返回的值添加更多级别的可选性。
换句话说:
- 如果您试图检索的类型不是可选的,它将因可选的链而成为可选的。
- 如果您试图检索的类型已经是可选的,它不会因为链而变得更加可选。
因此:
- 如果您尝试通过可选链检索
Int
值,则为Int?
无论使用多少级别的链条,总是会返回。 - 同样,如果您尝试检索
Int?
通过可选链获得价值,一个Int?
无论使用多少级别的链条,总是会返回。
下面的示例试图访问john``address
属性的street
财产。这里有两个级别的可选链条,用于链穿residence
和address
属性,两者都是可选类型:
- if let johnsStreet = john.residence?.address?.street {
- print(“John’s street name is (johnsStreet).”)
- } else {
- print(“Unable to retrieve the address.”)
- }
- // Prints “Unable to retrieve the address.”
john.residence
的值目前包含一个有效的Residence
实例。然而,john.residence.address
的价值目前为nil
。因此,给john.residence?.address?.street
的电话失败了。
请注意,在上面的示例中,您正在尝试检索street
属性的值。此属性的类型是String?
。因此,john.residence?.address?.street
的返回值也是String?
,尽管除了属性的基础可选类型外,还应用了两个级别的可选链。
如果您将实际Address
实例设置为john.residence.address
的值,并为地址的street
属性设置实际值,您可以通过多级可选链访问street
属性的值:
- let johnsAddress = Address()
- johnsAddress.buildingName = “The Larches”
- johnsAddress.street = “Laurel Street”
- john.residence?.address = johnsAddress
- if let johnsStreet = john.residence?.address?.street {
- print(“John’s street name is (johnsStreet).”)
- } else {
- print(“Unable to retrieve the address.”)
- }
- // Prints “John’s street name is Laurel Street.”
在本例中,设置john.residence``address
属性的尝试将成功,因为john.residence
的值目前包含一个有效的Residence
实例。
具有可选返回值的方法链
前面的示例展示了如何通过可选链检索可选类型属性的值。您还可以使用可选链调用返回可选类型值的方法,并在需要时链上该方法的返回值。
The example below calls the Address
class’s buildingIdentifier()
method through optional chaining. This method returns a value of type String?
. As described above, the ultimate return type of this method call after optional chaining is also String?
:
- if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
- print(“John’s building identifier is (buildingIdentifier).”)
- }
- // Prints “John’s building identifier is The Larches.”
如果您想对此方法的返回值执行进一步的可选链式,请在方法的括号后放置可选链问号:
- if let beginsWithThe =
- john.residence?.address?.buildingIdentifier()?.hasPrefix(“The”) {
- if beginsWithThe {
- print(“John’s building identifier begins with "The".”)
- } else {
- print(“John’s building identifier doesn’t begin with "The".”)
- }
- }
- // Prints “John’s building identifier begins with “The”.”
注意
In the example above, you place the optional chaining question mark after the parentheses, because the optional value you are chaining on is the buildingIdentifier()
method’s return value, and not the buildingIdentifier()
method itself.