可选链接

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

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

可选链接作为强制解包的替代方法

可以通过在可选值之后放置一个问号(?)来指定可选链接,如果可选值为nil,则在该值上调用属性,方法或下标都为nil。这与在可选值之后放置感叹号(!)以强制展开其值非常相似。主要区别在于可选链接在可选项为nil时优雅地失败,而强制解包在可选项为nil时触发运行时错误。

要反映可以在nil值上调用可选链接的事实,可选链接调用的结果始终是可选值,即使要查询的属性,方法或下标返回非可选值。可以使用此可选返回值来检查可选链接调用是否成功(返回的可选项包含值),或者由于链中的nil值(返回的可选值为nil)而未成功。

具体来说,可选链接调用的结果与预期返回值的类型相同,但包含在可选项中。通常返回的Int属性将在通过可选链接访问时返回Int?。

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

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

class Residence {
    var numberOfRooms = 1
}
class Person {
    var residence: Residence?
}

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

如果创建一个新Person实例,则其residence属性默认初始化为nil,因为它是可选的。在下面的代码,john具有residence的属性值nil:

let john = Person()

如果尝试访问此人residence的numberOfRooms属性,通过在其后放置感叹号residence强制展开其值,则会触发运行时错误,因为没有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.")
}

这告诉Swift在可选属性residence上并检索numberOfRooms如果存在residence的值。

由于尝试访问numberOfRooms可能会失败,因此可选的链接尝试返回值类型Int?或“可选Int”。如果residence是nil,如上面的例子中,这个可选的Int也将是nil,不可能的访问numberOfRooms反映这个事实。可选的Int是通过可选绑定解包可选整数到roomCount变量。

请注意,即使这numberOfRooms是非可选的,也是如此。通过可选链查询事实意味着调用numberOfRooms将始终返回Int?而不是Int。

可以将Residence实例分配给john.residence,以便它不再具有nil值:

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

为可选链接定义模型类

可以使用可选链接来调用多个类的属性,方法和下标。这可以深入查看相互关联类型的复杂模型中的子属性,并检查是否可以访问这些子属性上的属性,方法和下标。

下面的代码片段定义了四个模型类,用于几个后续示例,包括多级可选链接的示例。如上所述的Person和Residence模型添加Room和Address类,具有相关联的属性,方法,和下标。

该Person类和以前以同样的方式定义:

class Person {
    var residence: 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数组中访问索引处的room。

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

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

用于rooms数组的Room是有一个属性叫做name,以及一个初始化把该属性设置为一个合适的房间名称的简单类:

class Room {
    var 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有值则返回buildingName,如果buildingNumber、street两者都有值则返回他们的连接,否则返回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.")
}

由于john.residence是nil,这种可选的链接调用和以前以同样的方式会失败。

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

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

在此示例中,设置john.residence的address属性的将失败,因为john.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()函数未被调用,因为没有打印任何内容。

通过可选链接调用方法

可以使用可选链接在可选值上调用方法,并检查该方法调用是否成功。即使该方法没有定义返回值,也可以执行此操作。

Residence类的方法printNumberOfRooms()打印numberOfRooms当前值。以下是该方法的外观:

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

此方法未指定返回类型。但是,没有返回类型的函数和方法具有隐式返回类型Void,如函数无返回值中所述。这意味着它们返回值为()或空元组。

如果在带有可选链接的可选值上调用此方法,则方法的返回类型将是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.")
}

如果通过可选链接设置属性,情况也是如此。上述示例通过可选链接访问属性中的其设置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.")
}

通过可选链接访问下标

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

注意: 通过可选链接访问可选值的下标时,将问号放在下标括号之前,而不是之后。可选链接的问号始终紧跟在表达式的可选部分之后。

下面的示例尝试使用Residence类上定义的下标检索john.residence属性rooms数组中第一个元素的名称。因为当前为nil,john.residence下标调用失败:

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("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.")
}

访问可选类型的下标

如果下标返回一个可选类型的值 - 例如Swift Dictionary类型的下标 - 在下标的右括号后面加一个问号,以链接其可选的返回值:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72

上面的示例定义了一个名为testScores的字典,它包含两个将String键映射到Int值数组的键值对。该示例使用可选链接将"Dave"数组中的第一个项目设置为91; “Bev"数组中的第一项加1; 并尝试在键为"Brian"的数组中设置第一项。前两个调用成功,因为testScores字典包含"Dave"和"Bev”。第三个调用失败,因为testScores字典不包含"Brian"。

链接多级链接

可以将多个级别的可选链接链接在一起,以深入查看模型中更深层次的属性,方法和下标。但是,多级可选链接不会为返回值添加更多级别的可选值。

换而言之:

  • 如果要检索的类型不是可选的,则由于可选链接,它将成为可选类型。
  • 如果尝试检索的类型已经是可选的,则由于链接,它不会变得多级可选类型。

因此:

  • 如果尝试通过可选链接检索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.")
}

当前john.residence包含有效Residence实例值。但是,john.residence.address目前的值为nil。因此,调用john.residence?.address?.street失败。

请注意,在上面的示例中,尝试检索street属性的值。这个属性的类型是String?。因此,john.residence?.address?.street返回值也是String?,即使除了属性是可选类型之外还应用了两个级别的可选链接。

如果将实际Address实例设置为john.residence.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 {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.")
}

在此示例中,尝试设置john.residence的address属性将成功,因为当前值john.residence包含有效Residence实例。

使用可选返回值链接方法

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

下面的示例通过可选链接调用Address类的buildingIdentifier()方法。此方法返回值的类型为String?。如上所述,可选链接后调用方法的最终返回类型是String?:

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}

如果想在这个方法的返回值进行进一步的可选链接,将链接可选问号放在该方法的括号后:

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()方法本身。