第14天 可选类型、空值合并运算符
空引用(从字面上理解就是变量没有值)是托尼·霍尔(Tony Hoare)早在1965年发明的。事后当被问及此事时,他表示“我称其为我十亿美元的错误”,因为空引用引发了无数问题。
这是你学习Swift基础知识的最后一天,这一天将专门讲解Swift针对空引用的解决方案——即所谓的“可选类型(optionals)”。可选类型是一项非常重要的语言特性,但理解起来可能会有点费脑子——如果需要反复观看某些教程才能理解,也不必感到沮丧。
本质上,可选类型旨在解决“如果我们的变量根本没有任何值该怎么办?”这一问题。Swift希望确保我们所有的程序都尽可能安全,因此针对这种可能出现的情况,它提供了一些非常具体且至关重要的处理方法。
今天你需要完成五个教程的学习,在这些教程中,你将接触到可选类型、可选值解包、空值合并运算符等内容。观看完每个视频后(如果愿意,也可以阅读补充材料),会有一些简短的测试,帮助你确认自己是否理解了所学内容。
14.1 如何使用可选类型(Optionals)处理缺失数据
作者:Paul Hudson 2021 年 10 月 13 日 已针对 Xcode 16.4 更新*
Swift 语言注重可预测性,这意味着它会尽可能鼓励我们编写安全且符合预期行为的代码。你可能已经接触过抛出函数(throwing functions),而 Swift 还有另一种确保可预测性的重要机制 ——可选类型(optionals),其含义是 “某个值可能存在,也可能不存在”。
要理解可选类型的实际用途,我们可以看下面这段代码:
let opposites = [
"Mario": "Wario",
"Luigi": "Waluigi"
]
let peachOpposite = opposites["Peach"]我们先创建了一个 [String: String] 类型的字典,包含两个键值对:Mario 和 Luigi。随后尝试读取键为 “Peach” 的值(该键在字典中并不存在),且没有指定当数据缺失时返回的默认值。
执行这段代码后,peachOpposite 会是什么值呢?字典的类型是 [String: String](键和值均为字符串),但我们尝试读取的键并不存在 —— 如果此时返回一个字符串,逻辑上是不成立的。
Swift 的解决方案就是 “可选类型”:它用于表示 “数据可能存在,也可能不存在”。可选类型的核心语法是在数据类型后添加一个问号(?),因此在上述例子中,peachOpposite 的类型会是 String?(可选字符串),而非普通的 String(字符串)。
可选类型可以类比成一个 “可能装了东西,也可能空着的盒子”。例如,String? 意味着盒子里可能有一个字符串,也可能什么都没有 —— 这种 “无值” 的状态用特殊值 nil 表示。任何数据类型都可以声明为可选类型,包括 Int、Double、Bool,以及枚举、结构体、类的实例。
你可能会想:“既然如此,这到底带来了什么变化?之前是 String,现在是 String?,但实际编写代码时区别在哪里?”
关键在于:Swift 要求代码具备可预测性,因此不允许直接使用 “可能不存在” 的数据。对于可选类型,这意味着我们需要先 “解包(unwrap)”—— 打开盒子查看里面是否有值,如果有,就取出值再使用。
Swift 提供了两种主要的可选类型解包方式,其中最常用的是以下语法:
if let marioOpposite = opposites["Mario"] {
print("Mario 的对立面是 \(marioOpposite)")
}这种 if let 语法在 Swift 中非常常见,它将条件判断(if)与常量声明(let)结合起来,具体做了三件事:
- 从字典中读取可选类型的值;
- 如果可选类型中包含字符串(即有值),则执行 “解包”—— 将字符串赋值给
marioOpposite常量; - 条件判断成功(可选类型解包成功),执行代码块内部的逻辑。
只有当可选类型包含值时,代码块才会执行。当然,你也可以添加 else 块(它本质上就是一个普通的条件判断),例如:
var username: String? = nil
if let unwrappedName = username {
print("我们获取到了用户:\(unwrappedName)")
} else {
print("可选类型为空。")
}可以把可选类型理解成 “薛定谔的数据类型”:盒子里可能有值,也可能没有,只有检查后才能确定。
虽然目前看起来这些概念偏理论,但可选类型对编写高质量代码至关重要。要知道,可选类型表示 “数据可能存在或不存在”,而非可选类型(non-optionals)(如普通字符串、整数等)则表示 “数据必须存在”。
举个例子:如果我们有一个非可选类型的 Int,就意味着它一定包含一个整数(可能是 100 万,也可能是 0,但必然是有效的整数)。与之对比,一个被设为 nil 的可选类型 Int? 则没有任何值—— 它不是 0 或其他任何数字,而是 “空”。
同理:
- 非可选类型
String一定包含字符串(可能是 “Hello”,也可能是空字符串""),但这与设为nil的可选类型String?完全不同; - 非可选类型数组(如
[Int])可能包含一个或多个元素,也可能是空数组([]),但这与设为nil的可选类型数组([Int]?)也完全不同。
需要明确的是:
- 设为
nil的可选整数 ≠ 存储 0 的非可选整数; - 设为
nil的可选字符串 ≠ 存储空字符串的非可选字符串; - 设为
nil的可选数组 ≠ 存储空数组的非可选数组。
可选类型的 nil 代表 “完全没有数据”,无论数据是否为空(如空字符串、空数组),都不属于这种情况。
正如 泽夫・艾森伯格(Zev Eisenberg)所说:“Swift 并非引入了可选类型,而是引入了非可选类型。”
我们可以通过一个例子直观感受这一点:如果尝试将可选整数传入一个要求 “非可选整数” 的函数,代码会报错:
func square(number: Int) -> Int {
number * number
}
var number: Int? = nil
print(square(number: number)) // 编译报错Swift 会拒绝编译这段代码,因为可选整数需要先解包 —— 我们不能在需要 “非可选值” 的地方直接使用 “可选值”,否则若可选值为 nil,程序会出现问题。
因此,要使用这个可选值,必须先进行解包,例如:
if let unwrappedNumber = number {
print(square(number: unwrappedNumber))
}最后还有一个实用技巧:解包可选类型时,很常见的做法是将其解包到同名常量中。这在 Swift 中是完全允许的,避免了我们需要命名 unwrappedNumber 这类冗余的变量名。
用这种方式,我们可以将上面的代码重写为:
if let number = number {
print(square(number: number))
}初次接触时,这种写法可能会让人困惑 ——number 怎么可能既是可选类型又是非可选类型呢?其实不然:
这里的核心是 “变量遮蔽(shadowing)”:我们在条件代码块内部临时创建了一个同名的非可选常量,它只在代码块内部有效。而在代码块外部,number 仍然是原来的可选类型。因此,在代码块内部,我们使用的是解包后的非可选值(如 Int),而非外部的可选值(如 Int?)。
【可选阅读】为什么 Swift 要有可选类型(Optionals)?
作者:Paul Hudson 2021 年 10 月 25 日 本文已针对 Xcode 16.4 更新*
Swift 的可选类型是其最强大的特性之一,同时也是最容易让人困惑的特性之一。它们的核心作用很简单:允许我们表示 “数据缺失” 的状态 —— 比如一个字符串不只是空字符串,而是 “根本不存在”。
Swift 中任何数据类型都可以是可选类型:
- 一个普通整数可能是 0、-1、500 或其他任意范围内的数字。
- 而可选整数除了能表示所有普通整数的取值外,还可能是
nil—— 即 “不存在该值”。 - 一个普通字符串可能是 “Hello”、可能是莎士比亚的全部作品,也可能是
""(空字符串)。 - 而可选字符串除了能表示所有普通字符串的取值外,还可能是
nil。 - 一个自定义的
User结构体可能包含各种描述用户的属性。 - 而可选 User 结构体要么包含这些属性(即存在该用户),要么 “根本不存在”。
理解可选类型的关键,就在于区分 “该类型的所有可能取值” 和 “可能为 nil(不存在)” 这两种状态 —— 这一点有时有时候并不容易。
举个例子,布尔值(Bool)只有 true(真)和 false(假)两种状态。这意味着可选布尔值会有三种可能:true、false,或者 “两者都不是”—— 即 “不存在该值”。这种情况在直觉上可能有点难理解,因为按理说某个事物在某个时刻要么为真、要么为假,对吧?
那我问你:我喜欢巧克力吗?除非你是我的朋友,或者经常在 Twitter 上关注我,否则你无法给出确切答案 —— 你既不能肯定地说 “是(我喜欢巧克力)”,也不能肯定地说 “否(我不喜欢巧克力)”,因为你根本不知道。当然,你可以来问我答案,但在问之前,唯一稳妥的回答是 “我不知道”—— 这种 “不知道” 的状态,在 Swift 中就可以用 “值为 nil 的可选布尔值” 来表示。
还有一个容易混淆的点是空字符串 "":空字符串虽然 “不含内容”,但和 nil 完全不是一回事 —— 空字符串仍然是 “存在的字符串”,只是内容为空而已。
初学 Swift 时,可选类型可能会让你觉得很麻烦 —— 你可能会觉得 Swift 根本不需要这个特性,觉得它只会碍事,甚至每次必须用它的时候都会咬牙切齿。但请相信我:几个月后,你会彻底理解它的用处,甚至会纳闷 “以前没有可选类型的时候,我是怎么写代码的!”
【可选阅读】为什么 Swift 要求我们解包可选类型?
作者:Paul Hudson 2023 年 10 月 3 日 本文已针对 Xcode 16.4 更新*
Swift 的可选类型要么存储一个具体值(比如 5 或 “Hello”),要么 “完全没有值”。可想而知,要把两个数字相加,前提是这两个数字 “确实存在”—— 这就是为什么 Swift 不允许我们直接使用可选类型的值,除非先对其进行 “解包”:也就是先查看可选类型内部是否真的有值,再把值取出来使用。
在 Swift 中解包可选类型有多种方式,最常用的一种是 if let 语法,示例如下:
func getUsername() -> String? {
"Taylor"
}
if let username = getUsername() {
print("用户名是 \(username)")
} else {
print("没有用户名")
}上面的 getUsername() 函数返回一个 “可选字符串”(String?),这意味着它的返回值要么是一个字符串,要么是 nil。为了方便理解,我让这个函数始终返回一个具体值,但这并不会改变 Swift 对它的判定 —— 它的返回类型依然是可选字符串。
那一行 if let 代码其实包含了多个操作步骤:
- 调用
getUsername()函数; - 接收函数返回的可选字符串;
- 检查这个可选字符串内部是否有值;
- 由于确实有值(即 “Taylor”),会把这个值从可选类型中提取出来,赋值给一个新的常量
username; - 此时条件判定为 “真”,执行打印语句:“用户名是 Taylor”。
可见,if let 是处理可选类型的一种非常简洁的方式 —— 它同时完成了 “检查是否有值” 和 “提取值” 这两个操作。
实际上,if let 还有一个简化写法。原本你可能需要这样写:
if let number = number {
print(square(number: number))
}但现在可以简化成这样:
if let number {
print(square(number: number))
}两者的功能完全相同 —— 在 if 条件的代码块内部,会创建一个与原变量同名的 “解包后常量”(这种特性称为 “变量遮蔽”),只是少了一些重复代码而已。
可选类型最重要的特性,就是 Swift 不允许我们在未解包的情况下直接使用它的值。这为我们的应用提供了极大的安全性,因为它消除了 “值是否存在” 的不确定性:当你处理一个字符串时,你能确定它是一个有效的字符串;当你调用一个返回整数的函数时,你能确定这个整数可以直接安全使用。而当你的代码中确实存在可选类型时,Swift 总会确保你以正确的方式处理它们 —— 也就是先检查、再解包,而不是把 “可能不安全的值” 和 “确定安全的值” 混在一起使用。
【练习题】解包可选类型(Unwrapping Optionals)
问题 1/12:以下代码会打印一条消息,这种说法是对还是错?
var favoriteMovie: String? = nil // 声明可选字符串类型变量,初始值为nil
favoriteMovie = "The Life of Brian" // 给变量赋值
if let movie = favoriteMovie { // 可选绑定解包,若有值则进入分支
print("Your favorite movie is \(movie).") // 打印含电影名的消息
} else {
print("You don't have a favorite movie.") // 无值时打印的消息
}问题 2/12:以下代码会打印一条消息,这种说法是对还是错?
var weatherForecast: String = "sunny" // 声明非可选字符串变量,值为"sunny"
if let forecast = weatherForecast { // 对非可选类型进行可选绑定(语法允许但没必要)
print("The forecast is \(forecast).") // 打印含天气预报的消息
} else {
print("No forecast available.") // 非可选类型不会进入该分支
}问题 3/12:以下代码会打印一条消息,这种说法是对还是错?
let song: String? = "Shake it Off" // 声明可选字符串常量,值为"Shake it Off"
if let unwrappedSong = song { // 可选绑定解包,有值则进入分支
print("The name has \(unwrappedSong.count) letters.") // 打印歌曲名的字符数
}问题 4/12:以下代码会打印一条消息,这种说法是对还是错?
let currentDestination: String? = nil // 声明可选字符串常量,值为nil
if let destination = currentDestination { // 可选绑定解包,无值则进入else分支
print("We're walking to \(destination).") // 有值时打印的消息
} else {
print("We're just wandering.") // 无值时打印的消息
}问题 5/12:以下代码会打印一条消息,这种说法是对还是错?
let tableHeight: Double? = 100 // 声明可选Double类型常量,值为100
if tableHeight > 85.0 { // 错误:可选类型不能直接参与比较运算(未解包)
print("The table is too high.") // 代码无法编译,不会执行打印
}问题 6/12:以下代码会打印一条消息,这种说法是对还是错?
let menuItems: [String]? = ["Pizza", "Pasta"] // 声明可选字符串数组常量,含两个元素
if let items = menuItems { // 可选绑定解包,将值赋给items(但未使用items)
print("There are \(menuItems.count) items to choose from.") // 错误:menuItems仍为可选类型,不能直接调用count
}问题 7/12:以下代码会打印一条消息,这种说法是对还是错?
var score: Int = nil // 错误:非可选类型Int不能赋值为nil(编译失败)
score = 556
if let playerScore = score {
print("You scored \(playerScore) points.")
}问题 8/12:以下代码会打印一条消息,这种说法是对还是错?
let album = "Red" // 声明非可选字符串常量,值为"Red"
let albums = ["Reputation", "Red", "1989"] // 声明非可选字符串数组,含三个元素
if let position = albums.firstIndex(of: album) { // firstIndex返回可选Int(找到则为索引,否则为nil),此处能找到"Red"的索引1
print("Found \(album) at position \(position).") // 打印找到的专辑及对应索引
}问题 9/12:以下代码会打印一条消息,这种说法是对还是错?
let userAge: Int? = 38 // 声明可选Int类型常量,值为38
if let age = userAge { // 可选绑定解包,有值则进入分支
print("You are \(age) years old.") // 打印含年龄的消息
}问题 10/12:以下代码会打印一条消息,这种说法是对还是错?
let favoriteTennisPlayer: String? = "Andy Murray" // 声明可选字符串常量,值为"Andy Murray"
if player { // 错误:未定义变量player(应为favoriteTennisPlayer,且可选类型不能直接用于条件判断)
print("Let's watch \(player)'s highlights video on YouTube.") // 代码无法编译,不会执行打印
}问题 11/12:以下代码会打印一条消息,这种说法是对还是错?
var winner: String? = nil // 声明可选字符串变量,初始值为nil
winner = "Daley Thompson" // 给变量赋值
if let name = winner { // 可选绑定解包,有值则进入分支
print("And the winner is... \(name)!") // 打印含获胜者姓名的消息
}问题 12/12:以下代码会打印一条消息,这种说法是对还是错?
var bestScore: Int? = nil // 声明可选Int类型变量,初始值为nil
bestScore = 101 // 给变量赋值101
if bestScore > 100 { // 错误:可选类型不能直接参与比较运算(未解包)
print("You got a high score!") // 代码无法编译,不会执行打印
} else {
print("Better luck next time.")
}14.2 如何使用 guard 解包可选类型
作者:Paul Hudson 2021 年 10 月 25 日 本文已针对 Xcode 16.4 更新*
你已经了解到 Swift 中可以使用 if let 来解包可选类型,这也是使用可选类型最常见的方式。但还有第二种功能类似的方式,其使用频率也几乎与 if let 持平,那就是 guard let。
guard let 的用法如下:
func printSquare(of number: Int?) {
guard let number = number else {
print("缺少输入值")
return
}
print("\(number) × \(number) = \(number * number)")
}和 if let 一样,guard let 会检查可选类型内部是否包含值,如果包含值,就会提取该值并将其存入我们指定的常量中。
不过,二者的执行逻辑恰好相反:
var myVar: Int? = 3
if let unwrapped = myVar {
print("当 myVar 包含值时执行这段代码")
}
guard let unwrapped = myVar else {
print("当 myVar 不包含值时执行这段代码")
}也就是说,if let 会在可选类型包含值时执行其大括号内的代码,而 guard let 会在可选类型不包含值时执行其大括号内的代码。这也解释了代码中 else 的作用:“检查是否能解包可选类型,如果不能解包,就……”
我知道这看起来只是个微小的差异,但它会带来重要影响。要知道,guard 的核心作用是检查程序状态是否符合预期,如果不符合,就 “退出”—— 例如退出当前函数。
这种方式有时被称为 “提前返回”(early return):在函数一开始就检查所有输入是否有效,只要有任何一个输入无效,就执行一些处理代码,然后直接退出函数。如果所有检查都通过,函数就能按照预期正常执行。
guard 就是为这种编程风格设计的,实际上它还提供了两项辅助功能:
- 如果你使用
guard检查函数输入的有效性,Swift 会强制要求你在检查失败时使用return(退出函数)。 - 如果检查通过,且被解包的可选类型内部包含值,那么在
guard代码块执行结束后,你仍然可以使用这个解包后的值。
查看前面的 printSquare() 函数,就能看到这两项功能的实际应用:
func printSquare(of number: Int?) {
guard let number = number else {
print("缺少输入值")
// 1:此处必须退出函数
return
}
// 2:`number` 在 guard 代码块外部仍可使用
print("\(number) × \(number) = \(number * number)")
}总结一下:如果需要解包可选类型并对其进行处理,就使用 if let;如果需要确保可选类型一定包含值(否则就退出当前作用域),就使用 guard let。
提示:guard 可以用于任何条件判断,不一定局限于解包可选类型。例如,你可以使用 guard someArray.isEmpty else { return }(检查数组是否为空,若不为空则退出)。
【可选阅读】何时使用 guard let 而非 if let
作者:Paul Hudson 2021 年 11 月 8 日 本文已针对 Xcode 16.4 更新*
Swift 为我们提供了 if let 的替代方案 ——guard let,它同样可以在可选类型包含值时对其进行解包,但工作方式略有不同:guard let 的设计初衷是 “如果检查失败,就退出当前函数、循环或条件语句”,因此通过它解包后的值,在检查结束后仍然可以使用。
为了展示二者的差异,我们先定义一个返回 “生命的意义”(可选整数类型)的函数:
func getMeaningOfLife() -> Int? {
42
}接下来,在另一个名为 printMeaningOfLife() 的函数中使用它:
func printMeaningOfLife() {
if let name = getMeaningOfLife() {
print(name)
}
}这里使用了 if let,因此只有当 getMeaningOfLife() 返回整数(而非 nil)时,才会打印结果。
如果用 guard let 改写这段代码,会是这样:
func printMeaningOfLife() {
guard let name = getMeaningOfLife() else {
return
}
print(name)
}是的,代码稍微变长了,但有两个重要变化:
- 它让我们可以专注于 “理想路径”(happy path)—— 即函数在一切正常时的行为(此处为打印 “生命的意义”)。
guard强制要求我们在使用它时退出当前作用域,在这个例子中,意味着如果检查失败,我们必须从函数中返回。这是强制性要求:如果不写return,Swift 会编译报错。
通常会在方法开头使用一次或多次 guard,因为它可以用于预先验证某些条件是否成立。相比 “检查一个条件→执行一段代码→再检查另一个条件→执行另一段代码” 的写法,这种方式能让代码更易读。
因此,如果你只是想解包一些可选类型,就用 if let;但如果是要专门检查条件是否成立(确保后续代码能正常执行),就优先使用 guard let。
【练习题】使用 guard 解包
问题 1/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func double(number: Int?) -> Int? {
guard let number = number else {
return nil
}
return number * 2
}
let input = 5
if let doubled = double(number: input) {
print("\(input) 的两倍是 \(doubled)。")
}问题 2/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func playOpera(by composer: String?) -> String? {
let composer = composer else { // 注:此处代码存在语法错误,正确写法应为 guard let composer = composer else
return "请指定作曲家。"
}
if composer == "Mozart" {
return "《费加罗的婚礼》"
} else {
return nil
}
}
if let opera = playOpera(by: "Mozart") {
print(opera)
}问题 3/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func playScale(name: String?) {
guard let name = name { // 注:此处代码存在语法错误,guard 条件后需加 else
return
}
print("正在演奏 \(name) 调音阶。")
}
playScale(name: "C")问题 4/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func verify(age: Int?) -> Bool {
guard age >= 18 { // 注:此处代码存在两处错误:1. age 是可选类型,需先解包;2. guard 条件后需加 else
return true
} else {
return false
}
}
if verify(age: 28) {
print("你已成年。")
} else {
print("过几年再来吧。")
}问题 5/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func uppercase(string: String?) -> String? {
guard let string = string else {
return nil
}
return string.uppercased()
}
if let result = uppercase(string: "Hello") {
print(result)
}问题 6/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func isLongEnough(_ string: String?) -> Bool {
guard let string = string else { return false }
if string.count >= 8 {
return true
} else {
return false
}
}
if isLongEnough("Mario Odyssey") { // "Mario Odyssey" 长度为 12,满足 >=8 的条件
print("我们来玩这个吧!")
}问题 7/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func add3(to number: Int) -> Int {
guard let number = number else { // 注:此处代码存在语法错误,number 是非可选类型 Int,无需解包
return 3
}
return number + 3
}
let added = add3(to: 5)
print(added)问题 8/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func validate(password: String?) -> Bool {
guard let password = password else {
return false
}
if password == "fr0sties" {
print("认证成功!")
return true
}
return false
}
validate(password: "fr0sties") // 密码匹配,会执行 print 语句问题 9/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func test(number: Int?) {
guard let number = number else { return }
print("数字是 \(number)")
}
test(number: 42) // number 能成功解包,会执行 print 语句问题 10/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func username(for id: Int?) -> String { // 注:此处代码存在语法错误,返回值类型为非可选 String,但 else 分支返回 nil
guard let id = id else {
return nil
}
if id == 1989 {
return "Taylor Swift"
} else {
return nil
}
}
if let user = username(for: 1989) {
print("你好,\(user)!")
}问题 11/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func describe(occupation: String?) {
guard let occupation = occupation else {
print("你没有工作。")
return
}
print("你是一名 \(occupation)。")
}
let job = "engineer"
describe(occupation: job) // occupation 能成功解包,会执行 print 语句问题 12/12:以下代码会打印消息吗?(正确选 true,错误选 false)
func plantTree(_ type: String?) {
guard type else { // 注:此处代码存在语法错误,可选类型直接用于条件判断需显式检查是否为 nil(如 type != nil),或通过 guard let 解包
return
}
print("正在种植一棵 \(type)。")
}
plantTree("willow")14.3 如何使用空值合并运算符解包可选类型
作者:Paul Hudson 2021年10月13日 已针对 Xcode 16.4 更新
等等……Swift 居然还有第三种解包可选类型的方法?没错!而且它非常实用:它被称为空值合并运算符(nil coalescing operator),我们可以通过它解包可选类型,如果可选类型为空,还能提供一个默认值。
让我们先回顾一下基础内容:
let captains = [
"Enterprise": "Picard",
"Voyager": "Janeway",
"Defiant": "Sisko"
]
let new = captains["Serenity"]上面的代码从 captains 字典中读取了一个不存在的键,这意味着 new 会是一个被设为 nil 的可选字符串(String?)。
使用空值合并运算符(写法为 ??),我们可以为任何可选类型提供默认值,示例如下:
let new = captains["Serenity"] ?? "N/A"这段代码会从 captains 字典中读取对应的值并尝试解包。如果可选类型内部包含值,该值会被返回并存储到 new 中;如果可选类型为空(nil),则会使用 “N/A” 作为替代值。
这意味着无论可选类型包含什么 —— 是具体值还是 nil—— 最终结果都是 new 会是一个非可选的普通字符串。这个字符串可能是来自 captains 字典中的对应值,也可能是 “N/A”。
现在,你可能会想:我们难道不能在从字典读取值时直接指定默认值吗?如果你有这样的疑问,那你的想法完全正确:
let new = captains["Serenity", default: "N/A"]这种写法会产生完全相同的结果,这可能会让人觉得空值合并运算符毫无用处。但实际上,空值合并运算符不仅能用于字典,还能用于任何可选类型。
例如,数组的 randomElement() 方法会从数组中返回一个随机元素,但它返回的是可选类型 —— 因为你可能会对空数组调用这个方法。因此,我们可以使用空值合并运算符为其提供默认值:
let tvShows = ["Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"又或者,你有一个包含可选属性的结构体,并且希望在该属性缺失时提供一个合理的默认值:
struct Book {
let title: String
let author: String?
}
let book = Book(title: "Beowulf", author: nil)
let author = book.author ?? "Anonymous"
print(author)空值合并运算符在将字符串转换为整数时也很实用:这种转换会返回一个可选的 Int?,因为转换可能失败 —— 你提供的字符串可能不是有效的整数(比如 “Hello”)。此时我们可以使用空值合并运算符提供默认值,示例如下:
let input = ""
let number = Int(input) ?? 0
print(number)由此可见,只要你有一个可选类型,并且希望使用其内部的值(若存在),或在其为空时使用默认值,空值合并运算符就能派上用场。
【可选阅读】在 Swift 中何时应该使用空值合并运算符?
作者:Paul Hudson 2021 年 3 月 11 日 已针对 Xcode 16.4 更新
空值合并运算符允许我们尝试解包可选类型,但如果可选类型包含 nil,则会使用默认值。这在 Swift 中非常实用:虽然可选类型是一项很棒的特性,但通常情况下,使用非可选类型(比如一个确定的字符串,而非 “可能是字符串、也可能是 nil” 的可选字符串)会更方便,而空值合并运算符正是获取非可选类型的绝佳方式。
例如,如果你正在开发一个聊天应用,并且需要加载用户保存的消息草稿,你可能会写出这样的代码:
let savedData = loadSavedMessage() ?? ""因此,如果 loadSavedMessage() 返回一个包含字符串的可选类型,该字符串会被解包并存储到 savedData 中;但如果这个可选类型是 nil,Swift 会将 savedData 设为空字符串。无论哪种情况,savedData 最终都会是 String 类型,而非 String? 类型。
你也可以链式使用空值合并运算符(尽管这种用法并不常见)。如果你需要,像下面这样的代码是有效的:
let savedData = first() ?? second() ?? ""这段代码会先尝试调用 first(),如果其返回 nil,则尝试调用 second();如果 second() 也返回 nil,则会使用空字符串作为最终值。
记住,从字典中读取键值对时,返回的结果始终是可选类型。因此,你可能需要在这里使用空值合并运算符,以确保得到一个非可选类型的值:
let scores = ["Picard": 800, "Data": 7000, "Troi": 900]
let crusherScore = scores["Crusher"] ?? 0不过,这其实是个人偏好问题 —— 字典还提供了一种略有不同的方式,允许我们为 “键不存在” 的情况指定默认值:
let crusherScore = scores["Crusher", default: 0]你可以选择自己喜欢的方式 —— 从字典中读取值时,这两种写法没有本质区别。
【练习题】空值合并运算符
题目 1/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
let painter: String = "Leonardo da Vinci"
var artist: String = painter ?? "Unknown"题目 2/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
var bestPony: String? = "Pinkie Pie"
let selectedPony: String? == bestPony ?? nil题目 3/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
let lightsaberColor: String? = "green"
let color = lightsaberColor ?? "blue"题目 4/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
var captain: String? = "Kathryn Janeway"
let name = captain ?? "Anonymous"题目 5/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
let numberSum: Double? = 0.0
let sum: Double = numberSum ??题目 6/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
var conferenceName: String? = "WWDC"
var conference: String = conferenceName ?? nil题目 7/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
let planetNumber: Int? = 426
var destination = planetNumber ?? 3题目 8/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
let userID: Int? = 556
let id = userID ?? "Unknown"题目 9/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
let distanceRan: Double? = 0.5
let distance: Double = distanceRan ?? 0题目 10/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
var userOptedIn: Bool? = nil
var optedIn = userOptedIn ?? false题目 11/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
let jeansNumber: Int? = nil
let jeans = jeansNumber ? 501题目 12/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?
var selectedYear: Int? = nil
let actualYear = selectedYear ?? 198914.4 如何使用可选链处理多个可选值
作者:Paul Hudson 2021 年 10 月 13 日 已针对 Xcode 16.4 更新
可选链(Optional Chaining)是一种简化的语法,用于读取 “可选值内部的可选值”。听起来这可能是一种不常用的功能,但只要看一个例子,你就会明白它的实用性。
先看下面这段代码:
let names = ["Arya", "Bran", "Robb", "Sansa"]
let chosen = names.randomElement()?.uppercased() ?? "No one"
print("Next in line: \(chosen)")这段代码同时使用了两种可选值相关的特性:你可能已经了解过 “空值合并运算符(nil coalescing operator)”—— 它能在可选值为空时提供默认值;而在空值合并运算符之前,我们看到的 ? 后续代码就是可选链。
可选链的作用是让我们可以表达 “如果这个可选值包含非空值,就解包它,然后执行后续操作”,并且后续操作可以继续追加。在上面的例子中,我们的逻辑是 “如果能从数组中随机获取一个元素,就将它转为大写”。要注意,randomElement() 方法返回的是可选值,因为数组有可能是空的。
可选链的 “神奇之处” 在于:如果可选值为空,它会 “静默地不执行后续操作”—— 直接返回原本的空可选值,不会报错。这意味着,可选链的返回值始终是一个可选值,这也是为什么我们仍需要空值合并运算符来提供默认值的原因。
可选链可以根据需求无限延长,只要链条中的任意一个环节返回 nil,后续所有代码都会被忽略,最终整体返回 nil。
为了更深入地理解可选链的用法,我们举一个更复杂的例子:假设我们需要根据书籍作者的名字,将书籍按字母顺序排列。如果拆解这个需求,会涉及以下几个可选环节:
- 我们有一个可选的
Book结构体实例 —— 可能有要排序的书籍,也可能没有。 - 这本书可能有作者,也可能是匿名作品(作者信息为空)。
- 即使作者信息存在,对应的字符串也可能是空字符串或包含有效文本,因此不能保证一定能获取到首字母。
- 即使能获取到首字母,也需要将其转为大写(比如作者 “bell hooks” 的名字以小写开头,转为大写后才能正确排序)。
对应的代码实现如下:
struct Book {
let title: String
let author: String?
}
var book: Book? = nil
let author = book?.author?.first?.uppercased() ?? "A"
print(author)这段代码的逻辑可以解读为:“如果有书籍实例,且该书籍有作者信息,且作者名字有首字母,就将首字母转为大写并返回;否则返回 'A'”。
诚然,实际开发中不太可能需要 “嵌套这么多层” 的可选链,但这个例子能让你清晰看到:可选链的语法非常简洁!
【可选阅读】为什么可选链如此重要?
作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新
Swift 的可选链允许我们在一行代码中 “穿透” 多层可选值;只要其中任意一层为 nil,整行代码的结果就会变为 nil。
举一个简单的例子:假设我们有一组人名,需要根据他们姓氏的首字母来确定排序位置:
let names = ["Vincent": "van Gogh", "Pablo": "Picasso", "Claude": "Monet"]
let surnameLetter = names["Vincent"]?.first?.uppercased()在这段代码中,我们两次使用了可选链:
- 访问字典值
names["Vincent"]时使用可选链 —— 因为字典中可能不存在键为 “Vincent” 的条目; - 读取姓氏的首字符时使用可选链 —— 因为字符串有可能是空的。
可选链与空值合并运算符是 “绝佳搭档”:可选链帮我们穿透多层可选值,空值合并运算符则在任意一层可选值为空时提供合理的默认值。
回到上面的姓氏例子,我们可以在无法读取姓氏首字母时,自动返回 “?” 作为默认值:
let surnameLetter = names["Vincent"]?.first?.uppercased() ?? "?"【练习题】可选链
题目 1/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let names = ["Taylor", "Paul", "Adele"]
let lengthOfLast = names.last?.count?题目 2/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let credentials = ["twostraws", "fr0sties"]
let lowercaseUsername = credentials.first.lowercased()题目 3/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let songs: [String]? = [String]()
let finalSong = songs?.last?.uppercased()题目 4/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
func albumReleased(in year: Int) -> String? {
switch year {
case 2006: return "Taylor Swift"
case 2008: return "Fearless"
case 2010: return "Speak Now"
case 2012: return "Red"
case 2014: return "1989"
case 2017: return "Reputation"
default: return nil
}
}
let album = albumReleased(in: 2006)?.uppercased()题目 5/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let attendees: [String] = [String]()
let firstInLine = attendees?.first?.uppercased()题目 6/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let shoppingList = ["eggs", "tomatoes", "grapes"]
let firstItem = shoppingList.first?.appending(" are on my shopping list")题目 7/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let captains: [String]? = ["Archer", "Lorca", "Sisko"]?
let lengthOfBestCaptain = captains.last?.count题目 8/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
func loadForecast(for dayNumber: Int) -> String {
print("Forecast unavailable.")
return nil
}
let forecast = loadForecast(for: 3)?.uppercased()题目 9/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let capitals = ["Scotland": "Edinburgh", "Wales": "Cardiff"]
let scottishCapital = capitals["Scotland"]?.uppercased()题目 10/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let favoriteColors = ["Paul": "Red", "Charlotte": "Pink"]
let charlotteColor = favoriteColors["Charlotte"]?.lowercased()题目 11/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let opposites = ["hot": "cold", "near": "far"]
let oppositeOfLight = opposites["light"].uppercased()题目 12/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?
let racers = ["Hamilton", "Verstappen", "Vettel"]
let winnerWasVE = racers.first?.hasPrefix("Ve")14.5 如何使用可选类型处理函数失败情况
作者:Paul Hudson 2021 年 10 月 13 日 已针对 Xcode 16.4 更新
当我们调用一个可能抛出错误的函数时,要么使用 try 并妥善处理错误,要么在确定该函数不会失败的情况下使用 try!(但要承担判断错误时代码崩溃的风险)。(提示:try! 应极少使用。)
不过,还有一种替代方案:如果我们只关心函数是成功还是失败,可以使用可选 try(try?)让函数返回一个可选值。如果函数执行过程中没有抛出任何错误,该可选值会包含函数的返回值;如果抛出了任何错误,函数则会返回 nil。这意味着我们无法知道具体抛出了哪种错误,但很多时候这已经足够 —— 我们可能只关心函数是否执行成功。
具体用法如下:
enum UserError: Error {
case badID, networkFailed // 错误类型:ID 无效、网络请求失败
}
func getUser(id: Int) throws -> String {
throw UserError.networkFailed // 主动抛出“网络请求失败”错误
}
if let user = try? getUser(id: 23) {
print("用户:\(user)")
}上述代码中,getUser() 函数总会抛出 networkFailed 错误(仅用于测试场景),但我们并不关心具体抛出了哪种错误 —— 只关心调用是否返回了用户信息。
这正是 try? 的作用:它让 getUser() 函数返回一个可选字符串(String?),若有任何错误抛出则返回 nil。如果你需要知道具体的错误类型,这种方式并不适用,但很多场景下我们确实无需关心错误细节。
此外,你还可以将 try? 与空值合并运算符(??)结合使用,含义是 “尝试获取该函数的返回值,如果失败则使用默认值”。
但需注意:使用时需要在空值合并运算符前添加括号,让 Swift 明确你的意图。例如:
let user = (try? getUser(id: 23)) ?? "匿名用户"
print(user)try? 主要用于以下三种场景:
- 与
guard let结合使用:若try?调用返回nil,则退出当前函数。 - 与空值合并运算符结合使用:尝试执行某个操作,失败时提供默认值。
- 调用无返回值的抛出函数时,若完全不关心执行结果(例如写入日志文件或向服务器发送统计数据)。
【可选阅读】何时应使用可选 try(try?)
作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新
在 Swift 中,我们可以使用 do、try 和 catch 来执行可能抛出错误的函数,但还有一种替代方式:使用 try? 将抛出函数的调用结果转换为可选值。如果函数执行成功,返回值会是一个包含正常结果的可选值;如果执行失败,返回值则为 nil。
使用可选 try 有其优缺点,核心取决于 “错误信息对你的重要程度”。如果你只需要知道函数执行成功或失败(无需区分失败的具体原因),那么可选 try 会非常适用 —— 因为它能将复杂的错误处理简化为 “是否成功” 这一判断。
例如,无需编写如下代码:
do {
let result = try runRiskyFunction() // 执行有风险的函数
print(result)
} catch {
// 执行失败的处理逻辑
}你可以直接写成:
if let result = try? runRiskyFunction() {
print(result)
}如果你确实只需要 “成功 / 失败” 的判断,理论上可以让 runRiskyFunction() 直接返回可选值而非抛出错误。但 “函数抛出错误 + 调用时使用可选 try” 的组合,能为后续开发提供更大灵活性:当你编写一个抛出错误的函数后,在调用处可以根据实际需求,灵活选择使用 try/catch 或可选 try。
值得一提的是,我在自己的代码中经常使用可选 try—— 它能帮我聚焦当前要解决的问题,极大提升开发效率。
【练习题】可选 try
题目 1/6:关于抛出函数(throwing functions),下列说法正确的是?
- 选项 1:使用
try时,必须捕获所有可能抛出的错误。 - 选项 2:可能抛出错误的函数必须标记为
throws?。
题目 2/6:关于抛出函数,下列说法正确的是?
- 选项 1:如果使用
try且确定函数不会抛出错误,则无需捕获任何错误。 - 选项 2:如果使用
try!但函数实际抛出了错误,代码会崩溃。
题目 3/6:关于抛出函数,下列说法正确的是?
- 选项 1:会抛出错误的函数必须标记为
throws。 - 选项 2:抛出函数可以通过元组(tuple)抛出多个错误。
题目 4/6:关于抛出函数,下列说法正确的是?
- 选项 1:抛出函数绝不能调用其他抛出函数。
- 选项 2:可以将
try?与if let结合使用。
题目 5/6:关于抛出函数,下列说法正确的是?
- 选项 1:使用
try?会将函数的返回值转换为可选类型。 - 选项 2:如果使用
try?但函数实际抛出了错误,代码会崩溃。
题目 6/6:关于抛出函数,下列说法正确的是?
- 选项 1:使用
try时,必须要么捕获错误,要么将当前函数标记为throws。 - 选项 2:可以使用
try?!组合try?和try!的功能。
14.6 总结:可选类型
作者:Paul Hudson 2021 年 10 月 13 日 已针对 Xcode 16.4 更新
在这些章节中,我们介绍了 Swift 最重要的特性之一 —— 可选类型。尽管大多数人一开始会觉得可选类型难以理解,但几乎所有人最终都会认同它们在实际使用中的价值。
让我们回顾一下所学内容:
- 可选类型允许我们表示 “数据缺失” 的状态,这意味着我们可以明确表达 “这个整数没有值”—— 这与 0 这样的固定数值是完全不同的概念。
- 因此,所有非可选类型的变量 / 常量必然包含一个确定的值,即便是空字符串(
"")这样的值。 - 可选类型的 “解包”(unwrapping)指的是 “打开盒子查看内部内容” 的过程:如果盒子里有值,就取出该值供使用;如果没有值,盒子里就是
nil。 - 我们可以使用
if let在可选类型有值时执行一段代码,也可以使用guard let在可选类型没有值时执行一段代码 —— 但使用guard时,必须在执行完代码后退出当前函数(或代码块)。 - 空值合并运算符(nil coalescing operator)
??会对可选类型进行解包并返回其内部的值,如果可选类型为nil,则返回一个预设的默认值。 - 可选链(optional chaining)允许我们用简洁的语法访问 “嵌套在可选类型内部的可选值”。
- 如果一个函数可能抛出错误(throw errors),可以使用
try?将其转换为可选类型的返回结果 —— 执行后要么得到函数的返回值,要么在抛出错误时得到nil。
在 Swift 的语言特性中,可选类型的学习难度仅次于闭包(closures)。但我保证,只需几个月的实践,你就会纳闷:“以前没有它的时候,我是怎么写代码的?”
14.7 检测点 9
作者:Paul Hudson 2021 年 10 月 15 日 已针对 Xcode 16.4 更新
既然你已经对可选类型有了一定理解,现在不妨暂停几分钟,尝试一个小型编码挑战,看看自己记住了多少内容。
你的挑战任务是:编写一个函数,该函数接受一个 “可选的整数数组”(optional array of integers),并随机返回数组中的一个元素。如果数组不存在(为 nil)或为空数组,则返回 1 到 100 之间的一个随机数。
听起来可能很简单?但我还没说关键要求:这个函数必须用一行代码实现。注意,这不是让你先写多行代码再删除所有换行符 —— 你需要真正能在一行代码内完成整个逻辑。
我稍后会给出一些提示,但建议你先尝试自己解决。
还在看提示吗?好的,以下是一些提示:
- 函数的参数类型应为
[Int]?—— 即 “可能存在、也可能为nil的整数数组”。 - 函数需要返回一个非可选类型的
Int。 - 可以使用可选链调用数组的
randomElement()方法(即使数组是可选类型),该方法的返回值本身也是一个可选类型。 - 由于需要返回非可选的整数,你应该使用空值合并运算符(
??)来指定 “当数组为nil或为空时,返回 1 到 100 之间的随机数”。