【Swift學習】Swift編程之旅---可選鏈(二十一),swift之旅
可選鏈Optional Chaining是一種可以在當前值可能為nil的可選值上請求和調用屬性、方法及下標的方法。如果可選值有值,那麼調用就會成功;如果可選值是nil,那麼調用將返回nil。多個調用可以串連在一起形成一個調用鏈,如果其中任何一個節點為nil,整個調用鏈都會失敗,即返回nil。
通過在想調用的屬性、方法、或下標的可選值(optional value)後面放一個問號(?),可以定義一個可選鏈。這一點很像在可選值後面放一個歎號(!)來強制展開它的值。它們的主要區別在於當可選值為空白時可選鏈式調用只會調用失敗,然而強制展開將會觸發執行階段錯誤。
為了反映可選鏈式調用可以在空值(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屬性是可選的,john屬性將初始化為nil:
let john = Person()
如果使用歎號(!)強制展開獲得這個john的residence屬性中的numberOfRooms值,會觸發執行階段錯誤,因為這時residence沒有可以展開的值:
let roomCount = john.residence!.numberOfRooms// 這會引發執行階段錯誤
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.")}// 列印 “Unable to retrieve the number of rooms.”
在residence後面添加問號之後,Swift 就會在residence不為nil的情況下訪問numberOfRooms。
因為訪問numberOfRooms有可能失敗,可選鏈式調用會返回Int?類型,或稱為“可選的 Int”。如上例所示,當residence為nil的時候,可選的Int將會為nil,表明無法訪問numberOfRooms。訪問成功時,可選的Int值會通過可選綁定展開,並賦值給非可選類型的roomCount常量。
要注意的是,即使numberOfRooms是非可選的Int時,這一點也成立。只要使用可選鏈式調用就意味著numberOfRooms會返回一個Int?而不是Int。
可以將一個Residence的執行個體賦給john.residence,這樣它就不再是nil了:
john.residence = Residence()
john.residence現在包含一個實際的Residence執行個體,而不再是nil。如果你試圖使用先前的可選鏈式調用訪問numberOfRooms,它現在將傳回值為1的Int?類型的值:
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).” 為可選鏈式調用定義模型類
通過使用可選鏈式調用可以調用多層屬性、方法和下標。這樣可以在複雜的模型中向下訪問各種子屬性,並且判斷能否訪問子屬性的屬性、方法或下標。
下面這段代碼定義了四個模型類,這些例子包括多層可選鏈式調用。為了方便說明,在Person和Residence的基礎上增加了Room類和Address類,以及相關的屬性、方法以及下標。
Person類:
class Person { var residence: Residence?}
Residence類
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屬性被實現為計算型屬性,而不是儲存型屬性。numberOfRooms屬性簡單地返回rooms數組的count屬性的值。
Residence還提供了訪問rooms數組的捷徑,即提供可讀寫的下標來訪問rooms數組中指定位置的元素。
此外,Residence還提供了printNumberOfRooms()方法,這個方法的作用是列印numberOfRooms的值。
最後,Residence還定義了一個可選屬性address,其類型為Address?。Address類的定義在下面會說明。
Room類是一個簡單類,其執行個體被儲存在rooms數組中。該類只包含一個屬性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 && street != nil { return "\(buildingNumber) \(street)" } else { return nil } }}
Address類提供了buildingIdentifier()方法,傳回值為String?。 如果buildingName有值則返回buildingName。或者,如果buildingNumber和street均有值則返回buildingNumber。否則,返回nil。
通過可選鏈式調用訪問屬性
可以通過可選鏈式調用在一個可選值上訪問它的屬性,並判斷訪問是否成功。
下面的代碼建立了一個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.")}// 列印 “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。
上面代碼中的賦值過程是可選鏈式調用的一部分,這意味著可選鏈式調用失敗時,等號右側的代碼不會被執行。對於上面的代碼來說,很難驗證這一點,因為像這樣賦值一個常量沒有任何副作用。下面的程式碼完成了同樣的事情,但是它使用一個函數來建立Address執行個體,然後將該執行個體返回用於賦值。該函數會在返回前列印“Function was called”,這使你能驗證等號右側的代碼是否被執行。
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。這意味著沒有傳回值的方法也會返回(),或者說空的元組。
如果在可選值上通過可選鏈式調用來調用這個方法,該方法的傳回型別會是Void?,而不是Void,因為通過可選鏈式調用得到的傳回值都是可選的。這樣我們就可以使用if語句來判斷能否成功調用printNumberOfRooms()方法,即使方法本身沒有定義傳回值。通過判斷傳回值是否為nil可以判斷調用是否成功:
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.")}// 列印 “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.")}// 列印 “It was not possible to set the address.”
通過可選鏈式調用訪問下標
通過可選鏈式調用,我們可以在一個可選值上訪問下標,並且判斷下標調用是否成功。
注意
通過可選鏈式調用訪問可選值的下標時,應該將問號放在下標方括弧的前面而不是後面。可選鏈式調用的問號一般直接跟在可選運算式的後面。
下面這個例子用下標訪問john.residence屬性儲存區的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.")}// 列印 “Unable to retrieve the first room name.”
在這個例子中,問號直接放在john.residence的後面,並且在方括弧的前面,因為john.residence是可選值。
類似的,可以通過下標,用可選鏈式調用來賦值:
john.residence?[0] = Room(name: "Bathroom")
這次賦值同樣會失敗,因為residence目前是nil。
如果你建立一個Residence執行個體,並為其rooms數組添加一些Room執行個體,然後將Residence執行個體賦值給john.residence,那就可以通過可選鏈和下標來訪問數組中的元素:
let johnsHouse = Residence()johnsHouse.rooms.append(Room(name: "Living Room"))johnsHouse.rooms.append(Room(name: "Kitchen"))john.residence = johnsHouseif let firstRoomName = john.residence?[0].name { print("The first room name is \(firstRoomName).")} else { print("Unable to retrieve the first room name.")}// 列印 “The first room name is Living Room.”
訪問可選類型的下標
如果下標返回可選類型值,比如 Swift 中Dictionary類型的鍵的下標,可以在下標的結尾括弧後面放一個問號來在其可選傳回值上進行可選鏈式調用:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]testScores["Dave"]?[0] = 91testScores["Bev"]?[0]++testScores["Brian"]?[0] = 72// "Dave" 數組現在是 [91, 82, 84],"Bev" 數組現在是 [80, 94, 81]
上面的例子中定義了一個testScores數組,包含了兩個索引值對,把String類型的鍵映射到一個Int值的數組。這個例子用可選鏈式調用把"Dave"數組中第一個元素設為91,把"Bev"數組的第一個元素+1,然後嘗試把"Brian"數組中的第一個元素設為72。前兩個調用成功,因為testScores字典中包含"Dave"和"Bev"這兩個鍵。但是testScores字典中沒有"Brian"這個鍵,所以第三個調用失敗。
串連多層可選鏈式調用
可以通過串連多個可選鏈式調用在更深的模型層級中訪問屬性、方法以及下標。然而,多層可選鏈式調用不會增加傳回值的可選層級。
也就是說:
- 如果你訪問的值不是可選的,可選鏈式調用將會返回可選值。
- 如果你訪問的值就是可選的,可選鏈式調用不會讓可選傳回值變得“更可選”。
因此:
- 通過可選鏈式調用訪問一個
Int值,將會返回Int?,無論使用了多少層可選鏈式調用。
- 類似的,通過可選鏈式調用訪問
Int?值,依舊會返回Int?值,並不會返回Int??。
下面的例子嘗試訪問john中的residence屬性中的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.")}// 列印 “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執行個體,並且為address中的street屬性設定一個有效值,我們就能過通過可選鏈式調用來訪問street屬性:
let johnsAddress = Address()johnsAddress.buildingName = "The Larches"johnsAddress.street = "Laurel Street"john.residence?.address = johnsAddressif let johnsStreet = john.residence?.address?.street { print("John's street name is \(johnsStreet).")} else { print("Unable to retrieve the address.")}// 列印 “John's street name is Laurel Street.”
在上面的例子中,因為john.residence包含一個有效Residence執行個體,所以對john.residence的address屬性賦值將會成功。
在方法的可選傳回值上進行可選鏈式調用
上面的例子展示了如何在一個可選值上通過可選鏈式調用來擷取它的屬性值。我們還可以在一個可選值上通過可選鏈式調用來調用方法,並且可以根據需要繼續在方法的可選傳回值上進行可選鏈式調用。
在下面的例子中,通過可選鏈式調用來調用Address的buildingIdentifier()方法。這個方法返回String?類型的值。如上所述,通過可選鏈式調用來調用該方法,最終的傳回值依舊會是String?類型:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() { print("John's building identifier is \(buildingIdentifier).")}// 列印 “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 does not begin with \"The\".") }}// 列印 “John's building identifier begins with "The".”
注意
在上面的例子中,在方法的圓括弧後面加上問號是因為你要在buildingIdentifier()方法的可選傳回值上進行可選鏈式調用,而不是方法本身。