可選鏈?zhǔn)秸{(diào)用是一種可以在當(dāng)前值可能為 nil
的可選值上請(qǐng)求和調(diào)用屬性、方法及下標(biāo)的方法。如果可選值有值,那么調(diào)用就會(huì)成功;如果可選值是 nil
,那么調(diào)用將返回 nil
。多個(gè)調(diào)用可以連接在一起形成一個(gè)調(diào)用鏈,如果其中任何一個(gè)節(jié)點(diǎn)為 nil
,整個(gè)調(diào)用鏈都會(huì)失敗,即返回 nil
。
注意
Swift 的可選鏈?zhǔn)秸{(diào)用和 Objective-C 中向
nil
發(fā)送消息有些相像,但是 Swift 的可選鏈?zhǔn)秸{(diào)用可以應(yīng)用于任意類型,并且能檢查調(diào)用是否成功。
通過在想調(diào)用的屬性、方法,或下標(biāo)的可選值后面放一個(gè)問號(hào)(?
),可以定義一個(gè)可選鏈。這一點(diǎn)很像在可選值后面放一個(gè)嘆號(hào)(!
)來強(qiáng)制展開它的值。它們的主要區(qū)別在于當(dāng)可選值為空時(shí)可選鏈?zhǔn)秸{(diào)用只會(huì)調(diào)用失敗,然而強(qiáng)制展開將會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤。
為了反映可選鏈?zhǔn)秸{(diào)用可以在空值(nil
)上調(diào)用的事實(shí),不論這個(gè)調(diào)用的屬性、方法及下標(biāo)返回的值是不是可選值,它的返回結(jié)果都是一個(gè)可選值。你可以利用這個(gè)返回值來判斷你的可選鏈?zhǔn)秸{(diào)用是否調(diào)用成功,如果調(diào)用有返回值則說明調(diào)用成功,返回 nil
則說明調(diào)用失敗。
這里需要特別指出,可選鏈?zhǔn)秸{(diào)用的返回結(jié)果與原本的返回結(jié)果具有相同的類型,但是被包裝成了一個(gè)可選值。例如,使用可選鏈?zhǔn)秸{(diào)用訪問屬性,當(dāng)可選鏈?zhǔn)秸{(diào)用成功時(shí),如果屬性原本的返回結(jié)果是 Int
類型,則會(huì)變?yōu)?Int?
類型。
下面幾段代碼將解釋可選鏈?zhǔn)秸{(diào)用和強(qiáng)制展開的不同。
首先定義兩個(gè)類 Person
和 Residence
:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence
有一個(gè) Int
類型的屬性 numberOfRooms
,其默認(rèn)值為 1
。Person
具有一個(gè)可選的 residence
屬性,其類型為 Residence?
。
假如你創(chuàng)建了一個(gè)新的 Person
實(shí)例,它的 residence
屬性由于是可選類型而將被初始化為 nil
,在下面的代碼中,john
有一個(gè)值為 nil
的 residence
屬性:
let john = Person()
如果使用嘆號(hào)(!
)強(qiáng)制展開獲得這個(gè) john
的 residence
屬性中的 numberOfRooms
值,會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤,因?yàn)檫@時(shí) residence
沒有可以展開的值:
let roomCount = john.residence!.numberOfRooms
// 這會(huì)引發(fā)運(yùn)行時(shí)錯(cuò)誤
john.residence
為非 nil
值的時(shí)候,上面的調(diào)用會(huì)成功,并且把 roomCount
設(shè)置為 Int
類型的房間數(shù)量。正如上面提到的,當(dāng) residence
為 nil
的時(shí)候,上面這段代碼會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤。
可選鏈?zhǔn)秸{(diào)用提供了另一種訪問 numberOfRooms
的方式,使用問號(hào)(?
)來替代原來的嘆號(hào)(!
):
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
后面添加問號(hào)之后,Swift 就會(huì)在 residence
不為 nil
的情況下訪問 numberOfRooms
。
因?yàn)樵L問 numberOfRooms
有可能失敗,可選鏈?zhǔn)秸{(diào)用會(huì)返回 Int?
類型,或稱為“可選的 Int
”。如上例所示,當(dāng) residence
為 nil
的時(shí)候,可選的 Int
將會(huì)為 nil
,表明無法訪問 numberOfRooms
。訪問成功時(shí),可選的 Int
值會(huì)通過可選綁定展開,并賦值給非可選類型的 roomCount
常量。
要注意的是,即使 numberOfRooms
是非可選的 Int
時(shí),這一點(diǎn)也成立。只要使用可選鏈?zhǔn)秸{(diào)用就意味著 numberOfRooms
會(huì)返回一個(gè) Int?
而不是 Int
。
可以將一個(gè) Residence
的實(shí)例賦給 john.residence
,這樣它就不再是 nil
了:
john.residence = Residence()
john.residence
現(xiàn)在包含一個(gè)實(shí)際的 Residence
實(shí)例,而不再是 nil
。如果你試圖使用先前的可選鏈?zhǔn)秸{(diào)用訪問 numberOfRooms
,它現(xiàn)在將返回值為 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).”
通過使用可選鏈?zhǔn)秸{(diào)用可以調(diào)用多層屬性、方法和下標(biāo)。這樣可以在復(fù)雜的模型中向下訪問各種子屬性,并且判斷能否訪問子屬性的屬性、方法和下標(biāo)。
下面這段代碼定義了四個(gè)模型類,這些例子包括多層可選鏈?zhǔn)秸{(diào)用。為了方便說明,在 Person
和 Residence
的基礎(chǔ)上增加了 Room
類和 Address
類,以及相關(guān)的屬性、方法以及下標(biāo)。
Person
類的定義基本保持不變:
class Person {
var residence: Residence?
}
Residence
類比之前復(fù)雜些,增加了一個(gè)名為 rooms
的變量屬性,該屬性被初始化為 [Room]
類型的空數(shù)組:
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?
}
現(xiàn)在 Residence
有了一個(gè)存儲(chǔ) Room
實(shí)例的數(shù)組,numberOfRooms
屬性被實(shí)現(xiàn)為計(jì)算型屬性,而不是存儲(chǔ)型屬性。numberOfRooms
屬性簡(jiǎn)單地返回 rooms
數(shù)組的 count
屬性的值。
Residence
還提供了訪問 rooms
數(shù)組的快捷方式,即提供可讀寫的下標(biāo)來訪問 rooms
數(shù)組中指定位置的元素。
此外,Residence
還提供了 printNumberOfRooms
方法,這個(gè)方法的作用是打印 numberOfRooms
的值。
最后,Residence
還定義了一個(gè)可選屬性 address
,其類型為 Address?
。Address
類的定義在下面會(huì)說明。
Room
類是一個(gè)簡(jiǎn)單類,其實(shí)例被存儲(chǔ)在 rooms
數(shù)組中。該類只包含一個(gè)屬性 name
,以及一個(gè)用于將該屬性設(shè)置為適當(dāng)?shù)姆块g名的初始化函數(shù):
class Room {
let name: String
init(name: String) { self.name = name }
}
最后一個(gè)類是 Address
,這個(gè)類有三個(gè) String?
類型的可選屬性。buildingName
以及 buildingNumber
屬性分別表示大廈的名稱和號(hào)碼,第三個(gè)屬性 street
表示大廈所在街道的名稱:
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingName != nil {
return buildingName
} else if let buildingNumber = buildingNumber, let street = street {
return "\(buildingNumber) \(street)"
} else {
return nil
}
}
}
Address
類提供了 buildingIdentifier()
方法,返回值為 String?
。 如果 buildingName
有值則返回 buildingName
。或者,如果 buildingNumber
和 street
均有值,則返回兩者拼接得到的字符串。否則,返回 nil
。
正如 使用可選鏈?zhǔn)秸{(diào)用代替強(qiáng)制展開 中所述,可以通過可選鏈?zhǔn)秸{(diào)用在一個(gè)可選值上訪問它的屬性,并判斷訪問是否成功。
使用前面定義過的類,創(chuàng)建一個(gè) Person
實(shí)例,然后像之前一樣,嘗試訪問 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.”
因?yàn)?john.residence
為 nil
,所以這個(gè)可選鏈?zhǔn)秸{(diào)用依舊會(huì)像先前一樣失敗。
還可以通過可選鏈?zhǔn)秸{(diào)用來設(shè)置屬性值:
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
在這個(gè)例子中,通過 john.residence
來設(shè)定 address
屬性也會(huì)失敗,因?yàn)?john.residence
當(dāng)前為 nil
。
上面代碼中的賦值過程是可選鏈?zhǔn)秸{(diào)用的一部分,這意味著可選鏈?zhǔn)秸{(diào)用失敗時(shí),等號(hào)右側(cè)的代碼不會(huì)被執(zhí)行。對(duì)于上面的代碼來說,很難驗(yàn)證這一點(diǎn),因?yàn)橄襁@樣賦值一個(gè)常量沒有任何副作用。下面的代碼完成了同樣的事情,但是它使用一個(gè)函數(shù)來創(chuàng)建 Address
實(shí)例,然后將該實(shí)例返回用于賦值。該函數(shù)會(huì)在返回前打印“Function was called”,這使你能驗(yàn)證等號(hào)右側(cè)的代碼是否被執(zhí)行。
func createAddress() -> Address {
print("Function was called.")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress()
沒有任何打印消息,可以看出 createAddress()
函數(shù)并未被執(zhí)行。
可以通過可選鏈?zhǔn)秸{(diào)用來調(diào)用方法,并判斷是否調(diào)用成功,即使這個(gè)方法沒有返回值。
Residence
類中的 printNumberOfRooms()
方法打印當(dāng)前的 numberOfRooms
值,如下所示:
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
這個(gè)方法沒有返回值。然而,沒有返回值的方法具有隱式的返回類型 Void
,如 無返回值函數(shù) 中所述。這意味著沒有返回值的方法也會(huì)返回 ()
,或者說空的元組。
如果在可選值上通過可選鏈?zhǔn)秸{(diào)用來調(diào)用這個(gè)方法,該方法的返回類型會(huì)是 Void?
,而不是 Void
,因?yàn)橥ㄟ^可選鏈?zhǔn)秸{(diào)用得到的返回值都是可選的。這樣我們就可以使用 if
語句來判斷能否成功調(diào)用 printNumberOfRooms()
方法,即使方法本身沒有定義返回值。通過判斷返回值是否為 nil
可以判斷調(diào)用是否成功:
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.”
同樣的,可以據(jù)此判斷通過可選鏈?zhǔn)秸{(diào)用為屬性賦值是否成功。在上面的 通過可選鏈?zhǔn)秸{(diào)用訪問屬性 的例子中,我們嘗試給 john.residence
中的 address
屬性賦值,即使 residence
為 nil
。通過可選鏈?zhǔn)秸{(diào)用給屬性賦值會(huì)返回 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.”
通過可選鏈?zhǔn)秸{(diào)用,我們可以在一個(gè)可選值上訪問下標(biāo),并且判斷下標(biāo)調(diào)用是否成功。
注意
通過可選鏈?zhǔn)秸{(diào)用訪問可選值的下標(biāo)時(shí),應(yīng)該將問號(hào)放在下標(biāo)方括號(hào)的前面而不是后面??蛇x鏈?zhǔn)秸{(diào)用的問號(hào)一般直接跟在可選表達(dá)式的后面。
下面這個(gè)例子用下標(biāo)訪問 john.residence
屬性存儲(chǔ)的 Residence
實(shí)例的 rooms
數(shù)組中的第一個(gè)房間的名稱,因?yàn)?john.residence
為 nil
,所以下標(biāo)調(diào)用失敗了:
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.”
在這個(gè)例子中,問號(hào)直接放在 john.residence
的后面,并且在方括號(hào)的前面,因?yàn)?john.residence
是可選值。
類似的,可以通過下標(biāo),用可選鏈?zhǔn)秸{(diào)用來賦值:
john.residence?[0] = Room(name: "Bathroom")
這次賦值同樣會(huì)失敗,因?yàn)?residence
目前是 nil
。
如果你創(chuàng)建一個(gè) Residence
實(shí)例,并為其 rooms
數(shù)組添加一些 Room
實(shí)例,然后將 Residence
實(shí)例賦值給 john.residence
,那就可以通過可選鏈和下標(biāo)來訪問數(shù)組中的元素:
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.")
}
// 打印“The first room name is Living Room.”
如果下標(biāo)返回可選類型值,比如 Swift 中 Dictionary
類型的鍵的下標(biāo),可以在下標(biāo)的結(jié)尾括號(hào)后面放一個(gè)問號(hào)來在其可選返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// "Dave" 數(shù)組現(xiàn)在是 [91, 82, 84],"Bev" 數(shù)組現(xiàn)在是 [80, 94, 81]
上面的例子中定義了一個(gè) testScores
數(shù)組,包含了兩個(gè)鍵值對(duì),分別把 String
類型的鍵映射到一個(gè) Int
值的數(shù)組。這個(gè)例子用可選鏈?zhǔn)秸{(diào)用把 "Dave"
數(shù)組中第一個(gè)元素設(shè)為 91
,把 "Bev"
數(shù)組的第一個(gè)元素 +1
,然后嘗試把 "Brian"
數(shù)組中的第一個(gè)元素設(shè)為 72
。前兩個(gè)調(diào)用成功,因?yàn)?testScores
字典中包含 "Dave"
和 "Bev"
這兩個(gè)鍵。但是 testScores
字典中沒有 "Brian"
這個(gè)鍵,所以第三個(gè)調(diào)用失敗。
可以通過連接多個(gè)可選鏈?zhǔn)秸{(diào)用在更深的模型層級(jí)中訪問屬性、方法以及下標(biāo)。然而,多層可選鏈?zhǔn)秸{(diào)用不會(huì)增加返回值的可選層級(jí)。
也就是說:
因此:
Int
值,將會(huì)返回 Int?
,無論使用了多少層可選鏈?zhǔn)秸{(diào)用。Int?
值,依舊會(huì)返回 Int?
值,并不會(huì)返回 Int??
。下面的例子嘗試訪問 john
中的 residence
屬性中的 address
屬性中的 street
屬性。這里使用了兩層可選鏈?zhǔn)秸{(diào)用,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
現(xiàn)在包含一個(gè)有效的 Residence
實(shí)例。然而,john.residence.address
的值當(dāng)前為 nil
。因此,調(diào)用 john.residence?.address?.street
會(huì)失敗。
需要注意的是,上面的例子中,street
的屬性為 String?
。john.residence?.address?.street
的返回值也依然是 String?
,即使已經(jīng)使用了兩層可選鏈?zhǔn)秸{(diào)用。
如果為 john.residence.address
賦值一個(gè) Address
實(shí)例,并且為 address
中的 street
屬性設(shè)置一個(gè)有效值,我們就能過通過可選鏈?zhǔn)秸{(diào)用來訪問 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.")
}
// 打印“John's street name is Laurel Street.”
在上面的例子中,因?yàn)?john.residence
包含一個(gè)有效的 Address
實(shí)例,所以對(duì) john.residence
的 address
屬性賦值將會(huì)成功。
上面的例子展示了如何在一個(gè)可選值上通過可選鏈?zhǔn)秸{(diào)用來獲取它的屬性值。我們還可以在一個(gè)可選值上通過可選鏈?zhǔn)秸{(diào)用來調(diào)用方法,并且可以根據(jù)需要繼續(xù)在方法的可選返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用。
在下面的例子中,通過可選鏈?zhǔn)秸{(diào)用來調(diào)用 Address
的 buildingIdentifier()
方法。這個(gè)方法返回 String?
類型的值。如上所述,通過可選鏈?zhǔn)秸{(diào)用來調(diào)用該方法,最終的返回值依舊會(huì)是 String?
類型:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// 打印“John's building identifier is The Larches.”
如果要在該方法的返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用,在方法的圓括號(hào)后面加上問號(hào)即可:
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".”
注意
在上面的例子中,在方法的圓括號(hào)后面加上問號(hào)是因?yàn)槟阋?
buildingIdentifier()
方法的可選返回值上進(jìn)行可選鏈?zhǔn)秸{(diào)用,而不是buildingIdentifier()
方法本身。