Swift 可选链
可选链(Optional Chaining)是一种在值可能为 nil 时安全访问其属性、方法或下标的写法。
它把「先判断是不是 nil,再取值」压缩成一个问号:链上任意一环为 nil,整个表达式就返回 nil,程序不会崩溃。
可选链语法
在可选值后面紧跟一个问号 ?,然后照常写属性、方法或下标,这就是可选链。
下面先定义一套贯穿全文的模型类,它们构成一条 Person → Residence → Address → Room 的链。
实例
// 房间
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.residence | Residence? | Residence? |
person.residence?.numberOfRooms | Int | Int? |
person.residence?.printNumberOfRooms() | Void | Void? |
person.residence?[0].name | String | String? |
注意:即使
numberOfRooms本身是Int(非可选),通过可选链访问后得到的也是Int?。所以要取值时必须用if let或??把它解出来。
通过可选链访问属性
当 residence 为 nil 时,person.residence?.numberOfRooms 直接返回 nil,不会去访问一个空对象。
实例
// 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 判断方法到底有没有执行。
实例
// 通过可选链调用后变成 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 上,而不是作用在数组元素上。
实例
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 显式判断。
