16 Optional Chaining 可选链

可选链接是一个用于查询和调用属性、方法和下标的过程,这些属性、方法和下标当前可能为nil。如果可选项包含值,则属性、方法或下标调用成功;如果可选为nil,则属性、方法或下标调用返回nil。多个查询可以链接在一起,如果链中的任何链接为nil,整个链就会正常地失败。

Swift中的可选链接类似于Objective-C中的消息传递nil,但其方式适用于任何类型,并且可以检查成功或失败。

Optional Chaining as an Alternative to Forced Unwrapping可选链接作为强制展开的替代方法

您可以通过在可选值后面加上问号(?)来指定可选链接,如果可选值是非nil的,则您希望对其调用属性、方法或下标。这非常类似于在可选值后面放置感叹号(!)来强制展开其值。主要区别在于,可选链接在可选为nil时优雅地失败,而强制展开则在可选为nil时触发运行时错误。

为了反映可选链接可以对空值调用这一事实,可选链接调用的结果总是可选值,即使要查询的属性、方法或下标返回非可选值。您可以使用这个可选返回值来检查可选链接调用是否成功(返回的可选包含一个值),或者由于链中的nil值而没有成功(返回的可选值为nil)。

具体地说,可选链接调用的结果与预期返回值的类型相同,但是包装在可选函数中。通常返回整型数的属性会返回整型数吗?当通过可选链接访问时。

接下来的几个代码片段演示了可选链接与强制解包的区别,并使您能够检查是否成功。

首先,定义了Person和Residence两个类:

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Residence实例有一个名为numberOfRooms的Int属性,默认值为1。Person实例有一个可选的residence属性,类型为Residence?。

如果您创建一个新的Person实例,由于可选,它的residence属性默认初始化为nil。在下面的代码中,john的住宅物业值为nil:

let john = Person()

如果您试图访问此人住宅的numberOfRooms属性,在住宅后放置一个感叹号强制展开其值,您将触发运行时错误,因为此人并没有住宅:

let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

可选链接提供了访问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存在,则检索numberOfRooms的值。

因为访问numberOfRooms的尝试可能会失败,所以可选的链接尝试返回一个Int类型的值。,或“可选Int”。在上面的例子中,当residence为nil时,这个可选的Int也将为nil,以反映无法访问numberOfRooms这一事实。可选的Int通过可选绑定访问,以打开整数并将非可选值分配给roomCount变量。

注意,即使numberOfRooms是一个非可选的整型数,这也是正确的。它是通过一个可选链查询的,这意味着对numberOfRooms的调用总是返回一个Int?而不是Int。

您可以为john分配一个Residence实例。,使其不再为空值:

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)."

Defining Model Classes for Optional Chaining 为可选链接定义模型类

您可以使用可选的链接来调用超过一层的属性、方法和下标。这使您能够深入到相关类型的复杂模型中的子属性,并检查是否可以访问这些子属性上的属性、方法和下标。

下面的代码片段定义了四个模型类,用于后面的几个示例,包括多级可选链接的示例。这些类通过添加一个Room和Address类以及相关的属性、方法和下标扩展了上面的Person和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属性实现为计算属性,而不是存储属性。计算后的numberOfRooms属性只返回rooms数组中的count属性的值。

作为访问它的rooms数组的快捷方式,这个版本的Residence提供了一个读写下标,提供了对rooms数组中请求索引的访问。

这个版本的Residence还提供了一个名为printNumberOfRooms的方法,它只打印住宅中的房间数量。

最后,Residence定义了一个名为address的可选属性,其类型为address ?。此属性的Address类类型定义如下。

Room数组中使用的Room类是一个简单的类,它有一个名为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 let buildingNumber = buildingNumber, let street = street {
            return "\(buildingNumber) \(street)"
        } else if buildingName != nil {
            return buildingName
        } else {
            return nil
        }
    }
}

Address类还提供了一个名为buildingIdentifier()的方法,该方法的返回类型为String?。该方法检查地址的属性,如果它有值,则返回buildingName;如果两者都有值,则返回与street连接的buildingNumber,否则返回nil。

Accessing Properties Through Optional Chaining

通过可选链接访问属性

如可选链接作为强制展开的替代方法所示,可以使用可选链接访问可选值上的属性,并检查该属性访问是否成功。

使用上面定义的类创建一个新的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."

您还可以尝试通过可选的链接设置属性的值:

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

在本例中,address会失败(注意不是报错),因为residence还是nil,后面的就不再执行了。

赋值是可选链接的一部分,这意味着不计算=操作符右侧的任何代码。在前面的示例中,很难看出someAddress从未被计算过,因为访问常量没有任何副作用。下面的清单执行相同的赋值,但是它使用一个函数来创建地址。函数在返回值之前打印“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()函数,因为没有打印任何内容。

Calling Methods Through Optional Chaining 通过可选链接调用方法

可以使用可选链接对可选值调用方法,并检查该方法调用是否成功。即使该方法没有定义返回值,也可以这样做。

Residence类上的printNumberOfRooms()方法打印numberOfRooms的当前值。下面是这个方法的样子:

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

此方法不指定返回类型。但是,没有返回类型的函数和方法有一个隐式的Void返回类型,正如在没有返回值的函数中所描述的那样。这意味着它们返回一个值()或一个空元组。

如果使用可选链接对可选值调用此方法,该方法的返回类型将为空?,而不是Void,因为通过可选链接调用返回值时,返回值总是可选类型。这使您能够使用if语句来检查是否可以调用printNumberOfRooms()方法,即使该方法本身没有定义返回值。将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.")
}
// Prints "It was not possible to print the number of rooms."

如果您试图通过可选链接设置属性,也是一样的。上面的示例通过可选链接访问属性,尝试为john的住宅设置一个地址值, 即使住宅为nil。任何通过可选链接设置属性的尝试都会返回一个Void?类型的值,让你可以与空值比较,看看属性是否已成功设定:

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."

Accessing Subscripts Through Optional Chaining 通过可选链接访问下标

可以使用可选链接尝试从下标检索和设置可选值上的值,并检查该下标调用是否成功。

当通过可选链接访问可选值上的下标时,将问号放在下标的括号前,而不是括号后。可选链接问号总是紧跟在可选表达式的后面。

下面的示例尝试检索john住宅的rooms数组中的第一个房间的名称。使用在residence类上定义的下标来表示residence属性。因为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。

如果您创建并分配一个实际的Residence实例给john.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."

Accessing Subscripts of Optional Type 访问可选类型的下标

如果下标返回一个可选类型的值——比如Swift字典类型的键下标——在下标的右括号后面加一个问号,以链接到它的可选返回值:

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的字典,其中包含两个键-值对,它们将字符串键映射到一个Int值数组。该示例使用可选链接将“Dave”数组中的第一项设置为91;将“Bev”数组中的第一项增加1;并尝试为“Brian”键设置数组中的第一项。前两个调用成功,因为testScores字典包含“Dave”和“Bev”的键。第三个调用失败,因为testScores字典不包含“Brian”的键。

Linking Multiple Levels of Chaining 链接多个级别的链

可以将多个级别的可选链接链接在一起,从而深入到模型中的属性、方法和下标。但是,可选链接的多个级别不会向返回的值添加更多级别的可选性。

换句话说:

  • 如果要检索的类型不是可选的,则由于可选链接,它将成为可选的。
  • 如果您试图检索的类型已经是可选的,则不会因为链接而变得更加可选。

因此:

  • 如果您试图通过可选链接检索一个Int值,总是返回一个Int?,无论使用多少级别的链接。
  • 类似地,如果您试图通过可选链接检索一个Int?值,总是返回一个Int?,无论使用多少级别的链接。

下面的示例尝试访问john住宅的address属性的street属性。这里使用了两个级别的可选链接,用于链接住宅和地址属性,它们都是可选类型:

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?,尽管street属性之前有两个可选类型。

如果将实际地址实例设置为john.residence的值,并为地址的街道属性设置一个实际值,您可以通过多级可选链接访问街道属性的值:

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."

Chaining on Methods with Optional Return Values 链接具有可选返回值的方法

前面的示例展示了如何通过可选链接检索可选类型属性的值。您还可以使用可选链接来调用返回可选类型值的方法,并在需要时链接该方法的返回值。

下面的示例通过可选链接调用Address类的buildingIdentifier()方法。这个方法返回一个String?类型的值。如上所述,这个方法调用在可选链接之后的最终返回类型也是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 does not begin with \"The\".")
    }
}

注意:在上面的示例中,您将可选链接问号放在括号后面,因为您链接的可选值是buildingIdentifier()方法的返回值,而不是buildingIdentifier()方法本身。

<<返回目录

推荐阅读更多精彩内容