Swift Optional Chaining

Optional Chaining is a process for querying and calling properties, methods, and subscripts. The target of the query or call may be nil.

Optional chaining returns two values:

  • If the target has a value, the call succeeds and returns that value

  • If the target is nil, the call returns nil

Multiple requests or calls can be chained into a chain; if any node is nil, the entire chain fails.


Optional chaining can replace forced unwrapping

By placing a question mark (?) after the optional value of a property, method, or subscript, you can define an optional chain.

Optional chaining '?' Exclamation mark (!) force unwraps method, property, and subscript optional chains
? is placed after an optional value to call methods, properties, and subscripts ! is placed after an optional value to call methods, properties, and subscripts to force unwrap the value
When the optional is nil, output a friendlier error message When the optional is nil, force unwrapping triggers an execution error

Using Exclamation Mark (!) Optional Chaining Example

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person()

//将导致运行时错误
let roomCount = john.residence!.numberOfRooms

The output of the above program is:

fatal error: unexpectedly found nil while unwrapping an Optional value

Trying to use the exclamation mark (!) to force unwrap and obtain the numberOfRooms property value of this person's residence property will cause a runtime error, because there is no residence value available for unwrapping at this time.

Using Question Mark (?) Optional Chaining Example

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("不能查看房间号")
}

The output of the above program is:

Cannot view room number

Because this attempt to obtain numberOfRooms may fail, optional chaining returns an Int? type value, also called "optional Int". When residence is empty (as in the above example), the optional Int will be nil, so numberOfRooms cannot be accessed.

It should be noted that this also holds when numberOfRooms is a non-optional Int (Int?). As long as the request is made through optional chaining, it means that numberOfRooms always returns an Int? instead of an Int.


Defining Model Classes for Optional Chaining

You can use optional chaining to call properties, methods, and subscripts across multiple levels. This allows you to take advantage of the complex models between them to access lower-level properties and check whether such lower-level properties can be successfully accessed.

Example

Four model classes are defined, including multi-level optional chaining:

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
        }
    }
}

Calling Methods Through Optional Chaining

You can use optional chaining to call a method on an optional value and check whether the method call succeeds. Even if the method does not return a value, you can still use optional chaining to achieve this.

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("无法输出房间号")
}

The output of the above program is:

Unable to output room number

Use an if statement to check whether the printNumberOfRooms method can be called successfully: if the method call through optional chaining succeeds, the implicit return value of printNumberOfRooms will be Void; if it does not succeed, it will return nil.


Using Optional Chaining to Call Subscripts

You can use optional chaining to try to get a value from a subscript and check whether the subscript call succeeds; however, you cannot set a subscript through optional chaining.

Example 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("无法检索到房间")
}

The output of the above program is:

Unable to retrieve the room

In the subscript call, the optional chaining question mark is placed directly after john.residence and before the subscript brackets, because john.residence is the optional value that the optional chaining tries to obtain.

Example 2

In the example, create a Residence instance for john.residence, and there are one or more Room instances in his rooms array. Then you can use optional chaining to get the instances in the rooms array through the Residence subscript:

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("无法检索到地址。 ")
}

The output of the above program is:

John 所在的街道是 Laurel Street。

Accessing Subscripts Through Optional Chaining Calls

Through optional chaining calls, we can use subscripts to read or write an optional value, and determine whether the subscript call succeeds.

Example

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("无法检索到房间")
}

The output of the above program is:

The first room is named living room

Accessing Subscripts of an Optional Type

If a subscript returns a value of an optional type, such as the key subscript of Dictionary in Swift, you can place a question mark after the subscript's closing bracket to chain to the subscript's optional return value:

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]

In the above example, a testScores array is defined, containing two key-value pairs, mapping String type keys to an integer array.

This example uses optional chaining calls to set the first element of the "Dave" array to 91, increment the first element of the "Bev" array by 1, and then try to set the first element of the "Brian" array to 72.

The first two calls succeed because these two keys exist. But the key "Brian" does not exist in the dictionary, so the third call fails.


Connecting Multiple Levels of Chaining

You can connect multiple levels of optional chaining together to dig into properties, methods, and subscripts further down in a model. However, multi-level optional chaining cannot add more levels than the already returned optional value.

If you try to obtain an Int value through optional chaining, no matter how many levels of chaining you use, the return is always Int?. Similarly, if you try to obtain an Int? value through optional chaining, no matter how many levels of chaining you use, the return is always Int?.

Example 1

The following example tries to obtain the street property of the address in john's residence property. Here, two levels of optional chaining are used to link the residence and address properties, both of which are optional types:

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("不能检索地址")
}

The output of the above program is:

Unable to retrieve address

Example 2

If you set an instance of Address as the value of john.residence.address, and set an actual value for the street property of address, you can obtain this property value through multi-level optional chaining.

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("无法检索房间")
}

The output of the above example is:

The first room is the living room

Chaining Functions That Return Optional Values

We can also use optional chaining to call methods that return optional values, and can continue to chain on the optional value.

Example

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("未指定房间号")
}

The output of the above program is:

No room number specified
Other Extensions