什么是可选链(Optional Chaining)
一句话:"当某个实例可能是 nil 时,允许你用 问号? 一路点下去;只要链中任何一环为 nil,整条表达式就优雅地返回 nil,而不会崩溃。"
与"强制解包 !"的生死对比
| 写法 | 成功时 | 失败时 | 是否安全 |
|---|---|---|---|
a!.b |
拿到值 | 运行时崩溃 ❌ | 不安全 |
a?.b |
拿到值 | 返回 nil ✅ | 安全 |
swift
class Person {
var residence: Residence? // 可选类型,默认 nil
}
class Residence {
var numberOfRooms = 1
}
let john = Person()
// 1. 强制解包------危险
// let roomCount = john.residence!.numberOfRooms
// 运行时崩溃:Unexpectedly found nil while unwrapping
// 2. 可选链------安全
if let roomCount = john.residence?.numberOfRooms {
print("房间数:\(roomCount)") // 不会进来,因为 residence 是 nil
} else {
print("无法获取房间数") // 走到这里,安全!
}
可选链的 4 条核心规则
- 链上任意环节为 nil → 整条表达式立即返回 nil,后续代码不再执行。
- 返回值总是可选类型:即使原属性是
Int,经过可选链后也变成Int?。 - 可连续多级链(A?.B?.C),但不会叠加可选层数;
String?再多层链也是String?。 - 不仅能点属性,还能 调方法、取下标、赋值;失败时返回
Void?或nil。
完整模型:Person → Residence → Room / Address
swift
class Room {
let name: String
init(name: String) {
self.name = name
}
}
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
// 返回可选 String
func buildingIdentifier() -> String? {
if let name = buildingName {
return name
}
if let num = buildingNumber, let street = street {
return "\(num) \(street)"
}
return nil
}
}
class Residence {
var rooms: [Room] = []
var address: Address?
// 计算属性
var numberOfRooms: Int {
rooms.count
}
// 下标
subscript(i: Int) -> Room? {
get {
return i < rooms.count ? rooms[i] : nil
}
set {
if let newValue, i <= rooms.count {
if i == rooms.count {
rooms.append(newValue)
}
else {
rooms[i] = newValue
}
}
}
}
// 无返回值方法,默认是返回Void
func printNumberOfRooms() {
print("这个房子有 \(numberOfRooms) 个房间")
}
}
class Person {
var residence: Residence?
}
实战场景 1:访问属性
swift
let p = Person()
// residence 为 nil,链式失败 → 返回 nil
let roomCount: Int? = p.residence?.numberOfRooms
print(roomCount as Any) // nil
实战场景 2:调用方法
swift
// 失败时返回 Void?,可利用与 nil 比较
if p.residence?.printNumberOfRooms() == nil {
print("方法没执行,因为 residence 是 nil")
}
实战场景 3:通过下标读写
swift
p.residence?[0] = Room(name: "主卧") // 失败,不会崩溃
// 给 residence 赋值后再试
p.residence = Residence()
p.residence?[0] = Room(name: "主卧") // 成功添加
print(p.residence?.rooms.first?.name ?? "无房间") // 主卧
实战场景 4:多级链(链中链)
swift
p.residence?.address = Address()
p.residence?.address?.street = "Infinite Loop"
let streetName: String? = p.residence?.address?.street
print(streetName as Any) // Optional("Infinite Loop")
// 再深一层:调用返回可选值的方法
let id: String? = p.residence?.address?.buildingIdentifier()
print(id as Any) // nil(因为 buildingName/Number 都为空)
// 在方法返回后继续链
let firstChar: Character? = p.residence?.address?.buildingIdentifier()?.first
print(firstChar as Any) // nil
可选链的赋值操作也有返回值
A?.B = C 整体返回 Void?,可用来判断赋值是否成功。
swift
func createAddress() -> Address {
print("⚠️ 这行会打印吗?")
return Address()
}
// 赋值失败,createAddress() 不会执行
let result: Void? = (p.residence = nil)
(p.residence?.address = createAddress())
// 控制台无输出,证明短路了
常见踩坑与调试技巧
- 链太长看不清?用断点看每一步的中间值。
- 忘了返回值是可选?直接当非可选用会编译错误。
- 与
try?、as?混用时,注意可选层级不会叠加,但可读性会变差,建议拆行。 - 在 @objc 协议 或 KVO 中,可选链无法直接观察,需先解包再观察。
扩展场景:在日常业务里花式用链
- JSON 嵌套解析
swift
let city: String? = json["user"]?.["address"]?.["city"]?.stringValue
- 路由跳转判空
swift
if navigationController?.topViewController?.isKind(of: DetailVC.self) == true { ... }
- 链式动画
swift
view?.layer?.animate()?.next()?.start()
- Combine 管道
swift
publisher?.flatMap { $0.optionalField?.publisher } // 依旧只需一个 ?
为什么需要 ??
可选链让我们安全地拿到可选值,但业务里更常见的是:"拿不到就算了,给个备胎。"
这时空合运算符(Nil-Coalescing Operator)?? 就是最佳接盘侠。
?? 基础回顾
swift
let roomCount = john.residence?.numberOfRooms ?? 0
解读:
- 链成功 → 返回真实房间数
- 链任意环节为 nil → 返回 0
- 结果类型退化成非可选 Int,直接可用,无需再解包
6 个实战场景,把 ?? 用到极致
- 多级链 + 自定义默认值
swift
// 业务:显示"城市+街道",拿不到就显示"未知地址"
let addressText = p.residence?.address?.street ?? "未知地址"
// 再升一级:整条都为空时显示"火星"
let finalText = addressText.isEmpty ? "火星" : addressText
- 方法链返回值是可选
swift
// buildingIdentifier() 返回 String?
let badgeText = p.residence?.address?.buildingIdentifier() ?? "暂无门牌"
- 下标访问越界 or key 不存在
swift
let scores = ["Alice": [80, 90], "Bob": []]
let aliceFirst = scores["Alice"]?.first ?? 0 // 80
print(aliceFirst)
let bobFirst = scores["Bob"]?.first ?? 0 // 0(数组空)
print(bobFirst)
let cindyFirst = scores["Cindy"]?.first ?? 0 // 0(key 不存在)
print(cindyFirst)
- 与 try? 混用------解析 JSON 一行代码
swift
let username = (try? JSONDecoder().decode(User.self, from: data))?.name ?? "游客"
- 与 as? 混用------VC 安全取值
swift
let indexPath = tableView.indexPathForSelectedRow
let cell = tableView.cellForRow(at: indexPath ?? IndexPath(row: 0, section: 0))
- 链式动画缺省回调
swift
UIView.animate(
withDuration: 0.3,
animations: { self.view.alpha = 0 },
completion: { _ in
self.dismissAnimation?() ?? self.defaultDismiss() // 备胎动画
}
)
性能陷阱:?? 的右表达式何时执行?
?? 是短路的:
- 左值非 nil → 右值根本不会求值
- 左值 nil → 才会计算右值
因此可以放心把昂贵构造放在右边:
swift
// 数据库查询很耗资源,仅当缓存为 nil 时才查
let config = loadCache()?.config ?? loadFromDB()
与三目运算符的区别
| 写法 | 是否强制解包 | 可读性表现 |
|---|---|---|
a != nil ? a : b |
需要手动解包 | 啰嗦 |
a ?? b |
编译器自动处理 | 简洁 |
可选链 + ?? 的 3 条最佳实践
- 先链后合:把
??放在最外层,保证链的每一步都可读。 - 默认值类型匹配:Swift 类型推导严格,
Int??与Int不会自动合并。 - 日志友好:给默认值加前缀标识,方便灰度排查。
swift
let uid = user?.id ?? "unknown_uid"
一道面试真题
写出编译通过的表达式:在"链可能失败"且"失败后要抛错"的场景下,如何把 ?? 与 throw 结合?
一个答案:
swift
let url = Bundle.main.url(forResource: "config", withExtension: "json")
?? { throw AppError.missingConfig }()
利用立即执行闭包把 throw 包成表达式,满足 ?? 右侧要求。