可选类型链
可选类型链(Optional Chaining)
可选类型链是请求和调用可选类型(当前可能为nil)的属性、方法和下标的过程。如果可选类型有值,那么属性、方法或者下标的调用会成功;如果可选类型是nil,那么属性、方法、下标返回nil。多个这样的请求会被链接在一块,如果链上的任意内容是nil,整个链整体会优雅的失败。
NOTE
Swift的可选类型链和OC中的nil信息类似,不同的是适用任何类型,而且可以被验证是否成功。
可选类型链作为强制拆包的一种方案
如果想调用可选类型的属性、方法或者下标的非nil值, 在该可选类型值后跟一个问号(?)就指定了一个可选类型链。这个做法和为了强制解包一个可选类型在其后跟一个叹号(!)类似。这两者的区别是一旦可选类型是nil,可选类型链会优雅的失败,但强制解包会触发运行时错误。
为了表现可选类型链可以在一个nil值上被调用,一个可选类型链调用的结果始终都是一个可选类型值,尽管请求的属性、方法或者下标的返回是一个非nil的值。可以用这个返回的可选类型值检查可选类型链调用正确(返回的可选类型中含有值)还是调用失败(返回的可选类型值是nil)。
特别的,可选类型链的调用结果和预期的结果类型上一样,只是被可选类型包装了。如果通过可选类型链访问,一个通常返回是Int的属性将会返回一个Int?类型。
下面的几个代码片段展示了可选类型链和强制拆包的区别和调用结果检查。
首先,两个类Person和Residence被定义:
class Person { var residence: Residence? } class Residence { var numberOfRooms = 1 }
Residence 实例有唯一的一个Int属性叫做numberOfRooms,它的默认值是1.Person实例有一个可选的Residence?属性residence。
如果要创建一个新的Person实例,它的residence属性被默认初始化为nil,因为他是可选的。下面的代码,john有一个为nil值的residence属性:
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 { println("John's residence has \(roomCount) room(s).") } else { println("Unable to retrieve the number of rooms.") } // prints "Unable to retrieve the number of rooms."
这种写法告诉Swift要将可选的residence属性链上,如果residence属性有值存在,得到它的numberOfRooms属性值。
因为试图访问numberOfRooms属性有失败的可能,可选类型链尝试返回一个Int?(可选Int)值。像上面例子中,当residence是nil,这个可选Int将会是nil,表示没办法访问numberOfRooms。
这里尽管numberOfRooms是一个非可选的Int,但通过可选类型链调用numberOfRooms后将会返回一个Int?而非Int。
可以将一个Residence实例赋值给john.residence,所以它不再是nil了:
john.residence = Residence()
john.residence现在包含了一个具体的Residence实例了,而不是nil。如果试着像上面一样通过可选类型链访问numberOfRooms属性,它将会返回一个Int?,包含numberOfRooms的默认属性1:
if let roomCount = john.residence?.numberOfRooms { println("John's residence has \(roomCount) room(s).") } else { println("Unable to retrieve the number of rooms.") } // prints "John's residence has 1 room(s)."
给可选类型链定义模型类
可以对一层或多层的属性、方法或者下表采用可选类型链调用。这使得在复杂的相互关联的类型模型中钻取子属性变为可能,还可以检查这些子属性的属性、方法和下标能否被访问。
下面的代码片段为后续的多层可选类型链例子定义了四个类。这些类扩展了上面的Person和Residence模型,通过组合属性、方法和下标,添加了Room和Address类。
Person类的定义和上面一样:
class Person { var residence: Residence? }
Residencel类变得比上面更复杂。这次,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() { println("The number of rooms is \(numberOfRooms)") } var address: Address? }
因为这个版本的Residence存储了一个Room类型实例的数组,它的numberOfRooms属性以一个计算属性实现,而不再是一个存储属性了。计算属性numberOfRooms只是返回rooms数组的count属性的值。
为了方便访问其rooms数组,这个版本的Residence提供了一可读写的下标,通过制定索引的方式访问rooms数组。
这个版本的residence有一个叫做pringNumberOfRooms的方法,只是将residence中的房间数量打印出来。
最后,Residence定义了一个可选属性叫做address,是一个Address?类型的。Address类型在后面定义。
rooms数组中的用到的Room类是一个简单类,有一个叫做name的属性,它的构造方法给name属性设置了一个合适的房间名字:
class Room { let name: String init(name: String) { self.name = name } }
这个模型中最后的类是Address。这个类有三个String?类型的可选属性。前两个属性buildingName和buildingNumber,二者取其一作为特定建筑物的地址的一部分。第三个属性,street用来命名地址所含的街道:
class Address { var buildingName: String? var buildingNumber: String? var street: String? func buildingIdentifier() -> String? { if buildingName != nil { return buildingName } else if buildingNumber != nil { return buildingNumber } else { return nil } } }
Address类提供了一个方法叫做buildingIndentifier,这个方法返回一个Stirng?。这个方法会检查buildingName和buildingNumber属性,如果buildingName属性有值就返回它,如果没有而builderNumber有值就返回builderName,如果两个都没有值就返回nil。
通过可选类型链访问属性
像 可选类型链作为强制拆包的一种方案(Optional Chaining as an Alternative to Forced Unwrapping)一节中描述的一样,可以使用可选类型链来访问一个可选值的属性,还可以检查那个属性的访问是否正确。
使用上面定义的泪欻功能键一个Person实例,然后尝试像上面一样访问它的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.") } // 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。
通过可选类型链调用方法
可以使用可选类型链调用一个可选值上的方法,还可以检查这个方法调用是否成功。方法没有定义返回值也没有关系。
Residence类中的printNumberOfRooms方法打印当前numberOfRooms的值。这里是方法定义: func printNumberOfRooms() { println("The number of rooms is \(numberOfRooms)") }
这个方法没有指定返回类型。然而,函数和方法没有返回类型的情况下默认返回Void类型,就像 没有返回值的函数(Functions Without Return Values)一节描述的。这意味着这类函数返回一个()值,或者可说是一个空的元组。
如果用可选类型链调用了可选值上的方法,方法的返回类型将是Void?而不是Void,因为通过可选类型链调用的返回值始终都是可选类型的。这样就可以通过使用一个if语句判断是否可以调用printNumberOfRooms方法,尽管这个方法没有定义一个返回值。将调用printNumberOfRooms的结果和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.") } // prints "It was not possible to print the number of rooms."
同样的事情在通过可选类型链给属性赋值的时候也会发生。上面例子中 通过可选类型链访问属性(Accessing Properties Through Optional Chaining)试图给john.residence的address属性赋值,尽管residence属性是nil。通过可选类型链尝试给一个属性赋值返回了一个Void?类型,用它和nil做比较就可以判断属性赋值是否成功:
if (john.residence?.address = someAddress) != nil { println("It was possible to set the address.") } else { println("It was not possible to set the address.") } // prints "It was not possible to set the address."
通过可选类型链访问下标
通过可选类型链可以获取和设置一个可选值上的下标的值,还可以检查下标调用是否成功:
NOTE
当通过可选类型访问可选值上的下标,在下标的括号前使用问号而不要再其后。可选类型链的问号通常紧跟在可选表达式后。
下面的例子尝试用Residence类中定义的下标访问john.residence属性中rooms数组中的第一个房间的名字。因为john.residence现在是nil,所以下标调用失败:
if let firstRoomName = john.residence?[0].name { println("The first room name is \(firstRoomName).") } else { println("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。
如果创建一个Residence实例赋值给john.residence,放置一个或几个Room实例到它的rooms数组中,那么就可以通过可选类型链使用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 { println("The first room name is \(firstRoomName).") } else { println("Unable to retrieve the first room name.") } // prints "The first room name is Living Room."
访问可选类型的下标
如果下标返回以恶可选类型——比如Swift中的字典类型的键下标——在下标闭合的括号后放置一个问号,将它的可选类型返回值链接起来:
var testscores = ["Dave": [86,82,84],"Bev": [79,94,81]] testscores["Dave"]?[0] = 91 testscores["Bev"]?[0]++ 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”的键。第三个失败了,因为字典中没有包含“Brian”的键。
多层级可选类型链
可将多个层级的链起来构成可选类型链来钻取一个模型中更深处的属性、方法和下标。但是,多层级可选类型链不会增加返回值的可选类型层级。
另外一种说法:
如果要访问的不是可选类型,因为使用可选类型链,将会返回一个可选类型;
如果要访问的已经是可选类型了,使用可选类型,不会额外再添加一层可选;
因此:
如果尝试通过可选类型链访问一个Int值,总会返回一个Int?类型,不管可选类型链有多少层。
类似的,如果尝试通过可选类型链访问一个Int?值,总会返回一个Int?类型,不管可选类型链有多少层。
下面例子访问john的residence属性的address属性的street属性。这里使用两层可选类型链,将residence和address属性链接起来,它们两个都是可选类型的:
if let johnsStreet = john.residence?.address?.street { println("John's street name is \(johnsStreet).") } else { println("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?类型的,尽管已经通过了两层可选类型链。
如果给john.residence.address设置一个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 { println("John's street name is \(johnsStreet).") } else { println("Unable to retrieve the address.") } // prints "John's street name is Laurel Street."
为了给john.residence.address赋值一个Address实例,这里使用了一个叹号。因为john.residence属性是一个可选类型,所以在访问residence属性的address熟悉之前需要使用叹号对其解包获取其实际值。
用可选类型返回值链接方法
前面的例子展示了如何通过可选类型链获得可选类型的属性值。同样可以通过可选类型链调用一个方法(返回一个可选类型),如果需要还可以将方法返回值链接在链上。
下面的例子通过可选类型链调用了Address类的buildingIdentifier 方法。这个方法返回一个String?类型的值。就像上面描述的,在通过可选类型链调用后这个方法的最终返回结果仍然是String?:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() { println("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 { println("John's building identifier begins with \"The\".") } else { println("John's building identifier does not begin with \"The\".") } } // prints "John's building identifier begins with "The"."
NOTE 上面的例子中,在括号之后防止了问号,这是因为是将 buildingIdentifier方法的返回值的可选类型链接起来,而不是buildingIdentifier方法本身。