Swift Optional 用法总结

31 阅读8分钟

Swift Optional 用法总结

Optional(可选项)用于表示一个值“可能存在,也可能不存在”。它让缺失值成为类型系统的一部分,调用方必须明确处理 nil,从而减少空值导致的运行时错误。

1. Optional 的本质

Int? 是 Optional<Int> 的简写。可以把 Optional 理解为一个拥有两种状态的枚举:

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

因此:

var age: Int? = 18 // 等价于 Optional<Int>.some(18)

age = nil // 等价于 Optional<Int>.none

只有可选类型能保存 nil。普通的 Int、String 等非可选类型不能保存 nil。

未提供初始值的可选变量会自动初始化为 nil:

var name: String? // nil

2. 哪些操作会产生 Optional

转换、查找、字典下标、枚举原始值初始化和其他可能失败的操作通常返回可选项。

2.1 类型转换

let number = Int("123")        // Int?,Optional(123)
let invalidNumber = Int("abc") // Int?,nil

条件类型转换 as? 也返回可选项:

let object: Any = "Swift"
let text = object as? String // String?,Optional("Swift")

2.2 字典下标

let user = ["name": "Jack"]
let name = user["name"] // String?
let age = user["age"]   // String?,nil

即使键实际存在,普通字典下标的返回类型仍然是 Optional,因为编译器不能保证每一次查询都能找到对应的键。

2.3 枚举原始值初始化

enum Season: Int {
    case spring = 1, summer, autumn, winter
}
​
let spring = Season(rawValue: 1) // Season?,Optional(.spring)
let unknown = Season(rawValue: 6) // Season?,nil

2.4 用 Optional 表示函数可能失败

func value(at index: Int, in array: [Int]) -> Int? {
    guard array.indices.contains(index) else {
        return nil
    }
    return array[index]
}
​
let numbers = [1, 15, 40, 29]
print(value(at: 2, in: numbers) as Any)  // Optional(40)
print(value(at: 10, in: numbers) as Any) // nil

直接使用 array[index] 时,索引越界会导致运行时错误。使用 array.indices.contains(index) 检查边界,并用 nil 表示索引无效,会更安全。

3. 解包 Optional

Optional 不能直接当作普通值使用,需要先处理“有值”和“没有值”两种情况。

3.1 可选绑定:if let

if let number = Int("123") {
    print("转换成功:(number)")
} else {
    print("转换失败")
}

如果新常量与原可选变量同名,可以使用简写:

let name: String? = "Jack"
​
if let name {
    print(name) // 此处 name 是 String
}

解包后的值只在对应分支内部有效。

3.2 同时绑定多个 Optional

let userName: String? = "Lily"
let password: String? = "123456"
​
if let userName, let password {
    print("(userName) / (password)")
}

逗号连接的多个绑定相当于逻辑“并且”:所有 Optional 都有值时,绑定才会成功。

3.3 提前退出:guard let

guard let 适合先处理失败情况。成功解包后的值可以在当前作用域的后续代码中继续使用:

func login(parameters: [String: String]) {
    guard let userName = parameters["userName"] else {
        print("请输入用户名")
        return
    }
​
    guard let password = parameters["password"] else {
        print("请输入密码")
        return
    }
​
    print("用户名:(userName),密码:(password)")
}

guard 条件为 false 时必须离开当前作用域,常见方式包括 return、break、continue 和 throw。

原代码中调用方传入的键是 "userName",但 accoutLogin 读取的是 "username"。字典键区分大小写,因此会导致用户名绑定失败。

3.4 循环绑定:while let

var iterator = [10, 20, 30].makeIterator()
​
while let value = iterator.next() {
    print(value)
}

next() 返回 Element?,返回 nil 时循环结束。

3.5 使用 switch

因为 Optional 本质上是枚举,所以也可以通过模式匹配处理:

let score: Int? = 90
​
switch score {
case .some(let value):
    print("分数:(value)")
case .none:
    print("暂无分数")
}

.some(let value) 也可以写成 case let value?。

4. 空合并运算符 ??

a ?? b 表示:如果 a 有值,就解包并返回该值;如果 a 为 nil,就返回默认值 b。

  • a ?? b
  • a 是可选项
  • b 是可选项 或者 不是可选项
  • b 跟 a 的存储类型必须相同
  • 如果 a 不为nil,就返回 a
  • 如果 a 为nil,就返回 b
  • 如果 b 不是可选项,返回 a 时会自动解包
let a: Int? = 1
let b: Int? = 2
let c = a ?? b // c是Int? , Optional(1)
​
let a: Int? = nil
let b: Int? = 2
let c = a ?? b // c是Int? , Optional(2)
​
let a: Int? = nil
let b: Int? = nil
let c = a ?? b // c是Int? , nil
​
let a: Int? = 1
let b: Int = 2
let c = a ?? b // c是Int,2
​
let a: Int? = nil
let b: Int = 2
// 如果不使用??运算符
let c: Int
if let tmp = a {
    c = tmp
} else {
    c = b
}
​
let storedAge: Int? = nil
let age = storedAge ?? 18 // 18,类型为 Int

右侧默认值只会在左侧为 nil 时求值,具有短路特性。

4.1 连续使用多个 ??

let a: Int? = 1
let b: Int? = 2
let c = a ?? b ?? 3 // c是Int,1
​
let a: Int? = nil
let b: Int? = 2
let c = a ?? b ?? 3 // c是Int , 2
​
let a: Int? = nil
let b: Int? = nil
let c = a ?? b ?? 3 // c是Int , 3
​
let localName: String? = nil
let cachedName: String? = "Jack"
​
let displayName = localName ?? cachedName ?? "Guest"
// "Jack"

表达式会取得第一个非 nil 的值。

4.2 ?? 与 if let 配合

let first: Int? = nil
let second: Int? = 6
​
if let value = first ?? second {
    print(value) // 6
}

这表示 first 或 second 至少有一个值。

下面的写法含义不同:

if let first, let second {
    print(first, second)
}

它要求两个 Optional 都有值。

5. 可选链 ?.

可选链允许在不强制解包的情况下访问属性、调用方法或使用下标。如果链条中任何一环为 nil,整个表达式就返回 nil。

struct Address {
    var city: String
}
​
struct User {
    var address: Address?
}
​
var user: User? = User(address: Address(city: "Shanghai"))
​
let city = user?.address?.city
// String?,Optional("Shanghai")
​
user?.address?.city = "Beijing"
let uppercasedCity = user?.address?.city.uppercased()
// String?,Optional("BEIJING")

通过可选链访问原本非可选的结果时,结果会变成 Optional。连续使用多层可选链不会不断增加可选层级;上例结果是 String?,而不是 String???。

6. 强制解包与隐式解包

6.1 强制解包 !

let text: String? = "Swift"
print(text!) // Swift

如果值为 nil,强制解包会触发运行时错误。一般优先使用 if let、guard let、?? 或可选链;只有能够严格证明值一定存在时才使用 !。

6.2 隐式解包 Optional:T!

var title: String! = "Home"
let length: Int = title.count

String! 仍然是 Optional,但在需要普通 String 的上下文中会尝试自动解包。如果值为 nil,自动解包同样会崩溃。

它常见于初始化之后必定有值、但声明时暂时无法赋值的场景,例如部分 UIKit 的 IBOutlet。新代码中应尽量缩小其使用范围。

7. Optional 的转换

7.1 map

Optional 有值时执行转换,为 nil 时直接返回 nil:

let ageText: String? = "18"
let decorated = ageText.map { "年龄:($0)" } // String?,Optional("年龄:18")

7.2 flatMap

当转换闭包自身会返回 Optional 时,flatMap 可以避免产生双重 Optional:

let input: String? = "18"
​
let mapped = input.map { Int($0) }         // Int??
let flatMapped = input.flatMap { Int($0) } // Int?

7.3 集合的 compactMap

compactMap 不是 Optional 的方法,而是集合的转换方法。它会过滤转换失败产生的 nil:

let texts = ["10", "abc", "20"]
let numbers = texts.compactMap(Int.init) // [10, 20]

7.4 try?

对于只关心成功结果、不需要区分具体错误的可抛出操作,可以使用 try?:

import Foundation
​
let fileURL = URL(fileURLWithPath: "/tmp/data.bin")
let data = try? Data(contentsOf: fileURL) // Data?

操作成功时得到值,抛出错误时得到 nil。如果需要记录或根据错误类型采取不同措施,应使用 do-catch。

8. 双重 Optional:T??

Int?? 等价于 Optional<Optional<Int>>,可以表示三种不同状态:

  1. 外层为 nil。
  2. 外层有值,但内层为 nil。
  3. 外层有值,内层也有具体值。
let outerNil: Int?? = nil
​
let innerNil: Int? = nil
let nestedInnerNil: Int?? = innerNil
​
let nestedValue: Int?? = 18

它们的打印结果分别类似:

nil
Optional(nil)
Optional(Optional(18))

image.png

image.png ### 8.1 为什么会出现 `Optional(nil)`
var nm1: Int? = nil
var nm2: Int?? = nm1

nm1 的类型是 Optional<Int>,状态为 .none。

把 nm1 赋值给 Int?? 类型的 nm2 时,nm1 会成为外层 Optional 包装的值:

    nm2
    └── Optional<Int?>:.some
        └── Optional<Int>:.none

因此:

  • nm1 打印为 nil。
  • nm2 打印为 Optional(nil)。
  • Optional(nil) 表示外层有值、内层为 nil。

8.2 三种状态对照

代码类型内部结构打印结果
let a: Int? = nilInt?.nonenil
let b: Int? = 6Int?.some(6)Optional(6)
let c: Int?? = nilInt??.nonenil
let d: Int?? = aInt??.some(.none)Optional(nil)
let e: Int?? = bInt??.some(.some(6))Optional(Optional(6))

8.3 ?? 每次只处理一层


let value = nestedValue ?? 1 // Int?,Optional(18)
let fallback = outerNil ?? 1 // Int?,Optional(1)

因为左侧是 Int??,所以一次 ?? 解开外层后,结果仍然是 Int?。

如果最终需要普通 Int,还要再处理内层:

 let finalValue = (nestedInnerNil ?? nil) ?? 0     // Int,0

双重 Optional 有时用于区分“没有提供”“明确提供 nil”“提供具体值”三种状态。但普通业务代码通常不需要它。如果不是有意表达三态,应检查泛型、字典或 map 是否意外增加了 Optional 层级。

9. 打印 Optional

直接把 Optional 传给 print 可能产生“隐式转换为 Any”的编译器警告。调试时可以明确描述它:

let age: Int? = 18
​
print(String(describing: age)) // Optional(18)
print(age as Any)              // Optional(18)

正式展示给用户时,通常应该先解包或提供默认值:

print(age.map(String.init) ?? "未知")

10. 处理方式速查

方式适用场景
if let有值与没有值时执行不同分支
guard let值缺失就提前退出,后续逻辑使用解包后的值
while let重复取得 Optional,直到结果为 nil
??值缺失时使用默认值
?.沿属性、方法或下标链安全访问
map / flatMap对 Optional 做函数式转换
switch明确处理 .some 和 .none
!已经能够严格证明值绝不为 nil

11. 常见建议

  1. 优先让“不应该缺失”的数据使用非 Optional 类型。
  2. 缺少值就无法继续时,使用 guard let 提前退出。
  3. 有合理默认值时,使用 ??。
  4. 只需要沿对象关系安全访问时,使用可选链。
  5. 不要为了消除编译错误而随意添加 !。
  6. 注意字典键区分大小写,例如 "userName" 和 "username" 是不同的键。
  7. 遇到 T?? 时,先确认是否真的需要表达三种状态。