现在位置: 首页 > Swift 教程 > 正文

Swift 可选链

可选链(Optional Chaining)是一种在值可能为 nil 时安全访问其属性、方法或下标的写法。

它把「先判断是不是 nil,再取值」压缩成一个问号:链上任意一环为 nil,整个表达式就返回 nil,程序不会崩溃。


可选链语法

在可选值后面紧跟一个问号 ?,然后照常写属性、方法或下标,这就是可选链。

下面先定义一套贯穿全文的模型类,它们构成一条 Person → Residence → Address → Room 的链。

实例

import Foundation

// 房间
class Room {
    let name: String
    init(name: String) {
        self.name = name
    }
}

// 地址
class Address {
    var buildingName: String?       // 楼名,可选
    var buildingNumber: String      // 门牌号
    var street: String              // 街道
    init(buildingNumber: String, street: String) {
        self.buildingNumber = buildingNumber
        self.street = street
    }
    // 返回地址标识:有楼名就用「楼名 + 门牌号」
    func buildingIdentifier() -> String? {
        if let buildingName = buildingName {
            return "\(buildingName) \(buildingNumber)"
        } else {
            return buildingNumber
        }
    }
}

// 住所
class Residence {
    var rooms: [Room] = []          // 房间列表,默认空数组
    var address: Address?           // 地址,可能为 nil
    var numberOfRooms: Int {        // 计算属性:房间数量
        return rooms.count
    }
    subscript(i: Int) -> Room {     // 下标:按索引取房间
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房间数量:\(numberOfRooms)")
    }
}

// 人
class Person {
    var residence: Residence?       // 住所,可能为 nil
}

其中 residence 与 address 是可选类型,正是可选链要处理的对象。

可选链的结果总是可选值

可选链不会改变原始类型,只是在外面再包一层可选。

表达式成员原始类型可选链结果类型
person.residenceResidence?Residence?
person.residence?.numberOfRoomsIntInt?
person.residence?.printNumberOfRooms()VoidVoid?
person.residence?[0].nameStringString?

注意:即使 numberOfRooms 本身是 Int(非可选),通过可选链访问后得到的也是 Int?。所以要取值时必须用 if let 或 ?? 把它解出来。


通过可选链访问属性

当 residence 为 nil 时,person.residence?.numberOfRooms 直接返回 nil,不会去访问一个空对象。

实例

let runoob = Person()

// residence 还是 nil,整条链返回 nil
if let roomCount = runoob.residence?.numberOfRooms {
    print("Runoob 有 \(roomCount) 个房间")
} else {
    print("Runoob 没有住所")
}

// 给 residence 赋值后再试一次
let residence = Residence()
residence.rooms.append(Room(name: "客厅"))
residence.rooms.append(Room(name: "书房"))
runoob.residence = residence

if let roomCount = runoob.residence?.numberOfRooms {
    print("Runoob 有 \(roomCount) 个房间")
} else {
    print("Runoob 没有住所")
}

执行以上代码输出:

Runoob 没有住所
Runoob 有 2 个房间

注意:runoob 是用 let 声明的,但 runoob.residence = residence 依然可以编译。因为 let 只禁止改变 runoob 这个引用本身,类实例内部的属性仍然可以修改。


通过可选链调用方法

把问号放在方法名前面,方法就变成「存在才调用」。

调用成功时返回值被包装成 Void?,因此可以用 != nil 判断方法到底有没有执行。

实例

// printNumberOfRooms() 的返回类型是 Void,
// 通过可选链调用后变成 Void?,可以拿来和 nil 比较
if runoob.residence?.printNumberOfRooms() != nil {
    print("方法调用成功")
} else {
    print("方法调用失败")
}

// 没有住所的人
let nobody = Person()
if nobody.residence?.printNumberOfRooms() != nil {
    print("方法调用成功")
} else {
    print("方法调用失败")
}

执行以上代码输出:

房间数量:2
方法调用成功
方法调用失败

如果方法本身有返回值,比如 String,通过可选链调用后同样得到 String?。


通过可选链访问下标

下标的问号写在方括号前面:residence?[0]。

注意 ?[0] 里的问号作用在 residence 上,而不是作用在数组元素上。

实例

// 可选链访问下标:residence 为 nil 时返回 nil
if let firstRoomName = runoob.residence?[0].name {
    print("第一个房间是:\(firstRoomName)")
} else {
    print("无法取得第一个房间")
}

// rooms 本身不是可选类型,所以 rooms[0] 不需要问号
if let secondRoomName = runoob.residence?.rooms[1].name {
    print("第二个房间是:\(secondRoomName)")
} else {
    print("无法取得第二个房间")
}

执行以上代码输出:

第一个房间是:客厅
第二个房间是:书房

注意:可选链只能防止 nil 崩溃,防不住数组越界。如果 residence?.rooms[99] 里的下标超出范围,程序依然会崩溃。


多层可选链

一层可选链后面可以继续接可选链,形成多层链。

链上任何一环为 nil,整个表达式就短路返回 nil,后面的成员不会被访问。

实例

// 给住所配上地址
let address = Address(buildingNumber: "100", street: "www.runoob.com")
address.buildingName = "RUNOOB 大厦"
runoob.residence?.address = address

// 三层链:Person → Residence → Address → buildingIdentifier()
if let identifier = runoob.residence?.address?.buildingIdentifier() {
    print("地址标识:\(identifier)")
} else {
    print("无法取得地址标识")
}

// 换一个没有楼名的地址,buildingIdentifier() 走另一条分支
let simpleAddress = Address(buildingNumber: "200", street: "www.runoob.com")
runoob.residence?.address = simpleAddress
print("地址标识:\(runoob.residence?.address?.buildingIdentifier() ?? "无")")

// 把住所清空,链在第二层断掉
let emptyPerson = Person()
print("空住所的地址:\(String(describing: emptyPerson.residence?.address?.street))")

执行以上代码输出:

地址标识:RUNOOB 大厦 100
地址标识:200
空住所的地址:nil

可选链与空合运算符配合

可选链返回的永远是可选值,最常用的搭档就是空合运算符 ??。

?? 会在左侧为 nil 时给出一个默认值,让结果重新变成非可选类型。

实例

// 有住所:拿到真实数量
print("房间数量(默认 0):\(runoob.residence?.numberOfRooms ?? 0)")

// 没有住所:返回默认值 0
let emptyPerson = Person()
print("空住所房间数:\(emptyPerson.residence?.numberOfRooms ?? 0)")

// 用 ?? 给字符串兜底
print("街道:\(runoob.residence?.address?.street ?? "未填写")")
print("楼名:\(runoob.residence?.address?.buildingName ?? "未填写")")

执行以上代码输出:

房间数量(默认 0):2
空住所房间数:0
街道:www.runoob.com
楼名:未填写

需要区分「值为 nil」和「值为 0」时,就不能用 ?? 直接把结果压成非可选,而要改用 if let 判断。


可选链赋值

可选链也可以放在等号左边,用来给属性赋值。

链断了赋值就不执行,而且等号右边的表达式也不会被求值,不会产生多余的副作用。

实例

// 用计数器观察右侧表达式有没有被求值
var createdCount = 0
func makeAddress(_ street: String) -> Address {
    createdCount += 1
    return Address(buildingNumber: "1", street: street)
}

// residence 为 nil,链在第一步就断开
let someone = Person()
someone.residence?.address = makeAddress("不会被执行")
print("Address 被创建了 \(createdCount) 次")

// 先给 residence 赋值,可选链赋值才会生效
let other = Person()
other.residence = Residence()
other.residence?.address = makeAddress("www.runoob.com")
print("Address 被创建了 \(createdCount) 次")
print("other 的街道:\(other.residence?.address?.street ?? "无")")

执行以上代码输出:

Address 被创建了 0 次
Address 被创建了 1 次
other 的街道:www.runoob.com

利用这一点,可以给「对象存在时才需要做的准备工作」加上可选链,省掉一层 if 判断。


常见问题

下面几个问题在初学可选链时最常遇到。

为什么 Int 变成了 Int?

因为可选链的结果必须能表示「链断了」这种情况,所以无论成员原本是不是可选,结果一律再包一层可选。

取值时用 if let、guard let 或 ?? 解包即可。

可选链和强制解包 ! 怎么选

可选链在 nil 时安静返回 nil,强制解包在 nil 时直接崩溃。

只有当你百分百确定值不为 nil,或者已经用 if let 判断过,才考虑用 !。

方法调用的结果怎么判断

返回 Void 的方法通过可选链调用后得到 Void?,可以写成 person.residence?.printNumberOfRooms() != nil。

如果只关心调用有没有发生,这种写法比先判断 residence != nil 更紧凑。

可选链能不能连续用在不同类型的可选值上

可以。每一环只关心「当前这个可选值是不是 nil」,类型不同不影响链接。

比如 person.residence?.address?.buildingIdentifier() 里,三个环节分别是 Residence?、Address? 和返回值 String?。

可选链能替代空值检查吗

不能完全替代。可选链只处理「链上某一环是 nil」,不会检查数组越界、类型转换失败等其它运行时错误。

需要在这些场景下给出不同的分支逻辑时,仍然要用 if let、guard let 或 switch 显式判断。