top
Loading...
Swift 可選鏈

Swift 可選鏈

可選鏈(Optional Chaining)是一種可以請求和調用屬性、方法和子腳本的過程,用於請求或調用的目標可能為nil。

可選鏈返回兩個值:

  • 如果目標有值,調用就會成功,返回該值

  • 如果目標為nil,調用將返回nil

多次請求或調用可以被鏈接成一個鏈,如果任意一個節點為nil將導致整條鏈失傚。


可選鏈可替代強製解析

通過在屬性、方法、或下標腳本的可選值後面放一個問號(?),即可定義一個可選鏈。

可選鏈 '?' 感嘆號(!)強製展開方法,屬性,下標腳本可選鏈
? 放置於可選值後來調用方法,屬性,下標腳本 ! 放置於可選值後來調用方法,屬性,下標腳本來強製展開值
當可選為 nil 輸出比較友好的錯誤信息 當可選為 nil 時強製展開執行錯誤

使用感嘆號(!)可選鏈實例

class Person {
    var residence: Residence?
}
class Residence {
    var numberOfRooms = 1
}
let john = Person()
//將導致運行時錯誤
let roomCount = john.residence!.numberOfRooms

以上程序執行輸出結果為:

fatal error: unexpectedly found nil while unwrapping an Optional value

想使用感嘆號(!)強製解析獲得這個人residence屬性numberOfRooms屬性值,將會引發運行時錯誤,因為這時沒有可以供解析的residence值。

使用問號(?)可選鏈實例

class Person {
    var residence: Residence?
}
class Residence {
    var numberOfRooms = 1
}
let john = Person()
// 鏈接可選residence?屬性,如果residence存在則取回numberOfRooms的值
if let roomCount = john.residence?.numberOfRooms {
    print("John 的房間號為 \(roomCount)。")
} else {
    print("不能查看房間號")
}

以上程序執行輸出結果為:

不能查看房間號

因為這種嘗試獲得numberOfRooms的操作有可能失敗,可選鏈會返回Int?類型值,或者稱作"可選Int"。當residence是空的時候(上例),選擇Int將會為空,因此會出現無法訪問numberOfRooms的情況。

要注意的是,即使numberOfRooms是非可選Int(Int?)時這一點也成立。只要是通過可選鏈的請求就意味著最後numberOfRooms總是返回一個Int?而不是Int。


為可選鏈定義模型類

你可以使用可選鏈來多層調用屬性,方法,和下標腳本。這讓你可以利用它們之間的復雜模型來獲取更底層的屬性,併檢查是否可以成功獲取此類底層屬性。

實例

定義了四個模型類,其中包括多層可選鏈:

class Person {
    var residence: Residence?
}
// 定義了一個變量 rooms,它被初始化為一個Room[]類型的空數組
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}
// 模型中的最終類叫做Address
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
        }
    }
}

通過可選鏈調用方法

你可以使用可選鏈的來調用可選值的方法併檢查方法調用是否成功。即使這個方法沒有返回值,你依然可以使用可選鏈來達成這一目的。

class Person {
    var residence: Residence?
}
// 定義了一個變量 rooms,它被初始化為一個Room[]類型的空數組
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}
// 模型中的最終類叫做Address
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
        }
    }
}
let john = Person()

if ((john.residence?.printNumberOfRooms()) != nil) {
    print("輸出房間號")
} else {
    print("無法輸出房間號")
}

以上程序執行輸出結果為:

無法輸出房間號

使用if語句來檢查是否能成功調用printNumberOfRooms方法:如果方法通過可選鏈調用成功,printNumberOfRooms的隱式返回值將會是Void,如果沒有成功,將返回nil。


使用可選鏈調用下標腳本

你可以使用可選鏈來嘗試從下標腳本獲取值併檢查下標腳本的調用是否成功,然而,你不能通過可選鏈來設置下標腳本。

實例1

class Person {
    var residence: Residence?
}
// 定義了一個變量 rooms,它被初始化為一個Room[]類型的空數組
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}
// 模型中的最終類叫做Address
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
        }
    }
}
let john = Person()
if let firstRoomName = john.residence?[0].name {
    print("第一個房間名 \(firstRoomName).")
} else {
    print("無法檢索到房間")
}

以上程序執行輸出結果為:

無法檢索到房間

在下標腳本調用中可選鏈的問號直接跟在 circname.print 的後面,在下標腳本括號的前面,因為circname.print是可選鏈試圖獲得的可選值。

實例2

實例中創建一個Residence實例給john.residence,且在他的rooms數組中有一個或多個Room實例,那么你可以使用可選鏈通過Residence下標腳本來獲取在rooms數組中的實例了:

class Person {
    var residence: Residence?
}
// 定義了一個變量 rooms,它被初始化為一個Room[]類型的空數組
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}
// 模型中的最終類叫做Address
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
        }
    }
}
let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "客廳"))
johnsHouse.rooms.append(Room(name: "廚房"))
john.residence = johnsHouse
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence!.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
    print("John 所在的街道是 \(johnsStreet)。")
} else {
    print("無法檢索到地址。 ")
}

以上程序執行輸出結果為:

John 所在的街道是 Laurel Street。

通過可選鏈接調用來訪問下標

通過可選鏈接調用,我們可以用下標來對可選值進行讀取或寫入,併且判斷下標調用是否成功。

實例

class Person {
    var residence: Residence?
}
// 定義了一個變量 rooms,它被初始化為一個Room[]類型的空數組
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}
// 模型中的最終類叫做Address
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
        }
    }
}
let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "客廳"))
johnsHouse.rooms.append(Room(name: "廚房"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
    print("第一個房間名為\(firstRoomName)")
} else {
    print("無法檢索到房間")
}

以上程序執行輸出結果為:

第一個房間名為客廳

訪問可選類型的下標

如果下標返回可空類型值,比如Swift中Dictionary的key下標。可以在下標的閉合括號後面放一個問號來鏈接下標的可空返回值:

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類型的key映射到一個整形數組。

這個例子用可選鏈接調用把"Dave"數組中第一個元素設為91,把"Bev"數組的第一個元素+1,然後嘗試把"Brian"數組中的第一個元素設為72。

前兩個調用是成功的,因為這兩個key存在。但是key"Brian"在字典中不存在,所以第三個調用失敗。


連接多層鏈接

你可以將多層可選鏈連接在一起,可以掘取模型內更下層的屬性方法和下標腳本。然而多層可選鏈不能再添加比已經返回的可選值更多的層。

如果你試圖通過可選鏈獲得Int值,不論使用了多少層鏈接返回的總是Int?。 相似的,如果你試圖通過可選鏈獲得Int?值,不論使用了多少層鏈接返回的總是Int?。

實例1

下面的例子試圖獲取john的residence屬性里的address的street屬性。這里使用了兩層可選鏈來聯系residence和address屬性,它們兩者都是可選類型:

class Person {
    var residence: Residence?
}
// 定義了一個變量 rooms,它被初始化為一個Room[]類型的空數組
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}
// 模型中的最終類叫做Address
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
        }
    }
}
let john = Person()
if let johnsStreet = john.residence?.address?.street {
    print("John 的地址為 \(johnsStreet).")
} else {
    print("不能檢索地址")
}

以上程序執行輸出結果為:

不能檢索地址

實例2

如果你為Address設定一個實例來作為john.residence.address的值,併為address的street屬性設定一個實際值,你可以通過多層可選鏈來得到這個屬性值。

class Person {
   var 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("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
class Room {
    let name: String
    init(name: String) { self.name = name }
}
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
        }
    }
}
let john = Person()
john.residence?[0] = Room(name: "浴室")
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "客廳"))
johnsHouse.rooms.append(Room(name: "廚房"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
    print("第一個房間是\(firstRoomName)")
} else {
    print("無法檢索房間")
}

以上實例輸出結果為:

第一個房間是客廳

對返回可選值的函數進行鏈接

我們還可以通過可選鏈接來調用返回可空值的方法,併且可以繼續對可選值進行鏈接。

實例

class Person {
    var residence: Residence?
}
// 定義了一個變量 rooms,它被初始化為一個Room[]類型的空數組
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}
// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}
// 模型中的最終類叫做Address
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
        }
    }
}
let john = Person()
if john.residence?.printNumberOfRooms() != nil {
    print("指定了房間號)")
}  else {
    print("未指定房間號")
}

以上程序執行輸出結果為:

未指定房間號
北斗有巢氏 有巢氏北斗