Skip to content

第14天 可选类型、空值合并运算符

空引用(从字面上理解就是变量没有值)是托尼·霍尔(Tony Hoare)早在1965年发明的。事后当被问及此事时,他表示“我称其为我十亿美元的错误”,因为空引用引发了无数问题。

这是你学习Swift基础知识的最后一天,这一天将专门讲解Swift针对空引用的解决方案——即所谓的“可选类型(optionals)”。可选类型是一项非常重要的语言特性,但理解起来可能会有点费脑子——如果需要反复观看某些教程才能理解,也不必感到沮丧。

本质上,可选类型旨在解决“如果我们的变量根本没有任何值该怎么办?”这一问题。Swift希望确保我们所有的程序都尽可能安全,因此针对这种可能出现的情况,它提供了一些非常具体且至关重要的处理方法。

今天你需要完成五个教程的学习,在这些教程中,你将接触到可选类型、可选值解包、空值合并运算符等内容。观看完每个视频后(如果愿意,也可以阅读补充材料),会有一些简短的测试,帮助你确认自己是否理解了所学内容。

14.1 如何使用可选类型(Optionals)处理缺失数据

作者:Paul Hudson 2021 年 10 月 13 日 已针对 Xcode 16.4 更新*

Swift 语言注重可预测性,这意味着它会尽可能鼓励我们编写安全且符合预期行为的代码。你可能已经接触过抛出函数(throwing functions),而 Swift 还有另一种确保可预测性的重要机制 ——可选类型(optionals),其含义是 “某个值可能存在,也可能不存在”。

要理解可选类型的实际用途,我们可以看下面这段代码:

swift
let opposites = [
    "Mario": "Wario",
    "Luigi": "Waluigi"
]

let peachOpposite = opposites["Peach"]

我们先创建了一个 [String: String] 类型的字典,包含两个键值对:Mario 和 Luigi。随后尝试读取键为 “Peach” 的值(该键在字典中并不存在),且没有指定当数据缺失时返回的默认值。

执行这段代码后,peachOpposite 会是什么值呢?字典的类型是 [String: String](键和值均为字符串),但我们尝试读取的键并不存在 —— 如果此时返回一个字符串,逻辑上是不成立的。

Swift 的解决方案就是 “可选类型”:它用于表示 “数据可能存在,也可能不存在”。可选类型的核心语法是在数据类型后添加一个问号(?),因此在上述例子中,peachOpposite 的类型会是 String?(可选字符串),而非普通的 String(字符串)。

可选类型可以类比成一个 “可能装了东西,也可能空着的盒子”。例如,String? 意味着盒子里可能有一个字符串,也可能什么都没有 —— 这种 “无值” 的状态用特殊值 nil 表示。任何数据类型都可以声明为可选类型,包括 IntDoubleBool,以及枚举、结构体、类的实例。

你可能会想:“既然如此,这到底带来了什么变化?之前是 String,现在是 String?,但实际编写代码时区别在哪里?”

关键在于:Swift 要求代码具备可预测性,因此不允许直接使用 “可能不存在” 的数据。对于可选类型,这意味着我们需要先 “解包(unwrap)”—— 打开盒子查看里面是否有值,如果有,就取出值再使用。

Swift 提供了两种主要的可选类型解包方式,其中最常用的是以下语法:

swift
if let marioOpposite = opposites["Mario"] {
    print("Mario 的对立面是 \(marioOpposite)")
}

这种 if let 语法在 Swift 中非常常见,它将条件判断(if)与常量声明(let)结合起来,具体做了三件事:

  1. 从字典中读取可选类型的值;
  2. 如果可选类型中包含字符串(即有值),则执行 “解包”—— 将字符串赋值给 marioOpposite 常量;
  3. 条件判断成功(可选类型解包成功),执行代码块内部的逻辑。

只有当可选类型包含值时,代码块才会执行。当然,你也可以添加 else 块(它本质上就是一个普通的条件判断),例如:

swift
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 并非引入了可选类型,而是引入了非可选类型。”

我们可以通过一个例子直观感受这一点:如果尝试将可选整数传入一个要求 “非可选整数” 的函数,代码会报错:

swift
func square(number: Int) -> Int {
    number * number
}

var number: Int? = nil
print(square(number: number)) // 编译报错

Swift 会拒绝编译这段代码,因为可选整数需要先解包 —— 我们不能在需要 “非可选值” 的地方直接使用 “可选值”,否则若可选值为 nil,程序会出现问题。

因此,要使用这个可选值,必须先进行解包,例如:

swift
if let unwrappedNumber = number {
    print(square(number: unwrappedNumber))
}

最后还有一个实用技巧:解包可选类型时,很常见的做法是将其解包到同名常量中。这在 Swift 中是完全允许的,避免了我们需要命名 unwrappedNumber 这类冗余的变量名。

用这种方式,我们可以将上面的代码重写为:

swift
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 语法,示例如下:

swift
func getUsername() -> String? {
    "Taylor"
}

if let username = getUsername() {
    print("用户名是 \(username)")
} else {
    print("没有用户名")
}

上面的 getUsername() 函数返回一个 “可选字符串”(String?),这意味着它的返回值要么是一个字符串,要么是 nil。为了方便理解,我让这个函数始终返回一个具体值,但这并不会改变 Swift 对它的判定 —— 它的返回类型依然是可选字符串。

那一行 if let 代码其实包含了多个操作步骤:

  1. 调用 getUsername() 函数;
  2. 接收函数返回的可选字符串;
  3. 检查这个可选字符串内部是否有值;
  4. 由于确实有值(即 “Taylor”),会把这个值从可选类型中提取出来,赋值给一个新的常量 username
  5. 此时条件判定为 “真”,执行打印语句:“用户名是 Taylor”。

可见,if let 是处理可选类型的一种非常简洁的方式 —— 它同时完成了 “检查是否有值” 和 “提取值” 这两个操作。

实际上,if let 还有一个简化写法。原本你可能需要这样写:

swift
if let number = number {
    print(square(number: number))
}

但现在可以简化成这样:

swift
if let number {
    print(square(number: number))
}

两者的功能完全相同 —— 在 if 条件的代码块内部,会创建一个与原变量同名的 “解包后常量”(这种特性称为 “变量遮蔽”),只是少了一些重复代码而已。

可选类型最重要的特性,就是 Swift 不允许我们在未解包的情况下直接使用它的值。这为我们的应用提供了极大的安全性,因为它消除了 “值是否存在” 的不确定性:当你处理一个字符串时,你能确定它是一个有效的字符串;当你调用一个返回整数的函数时,你能确定这个整数可以直接安全使用。而当你的代码中确实存在可选类型时,Swift 总会确保你以正确的方式处理它们 —— 也就是先检查、再解包,而不是把 “可能不安全的值” 和 “确定安全的值” 混在一起使用。

【练习题】解包可选类型(Unwrapping Optionals)

问题 1/12:以下代码会打印一条消息,这种说法是对还是错?

swift
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:以下代码会打印一条消息,这种说法是对还是错?

swift
var weatherForecast: String = "sunny"  // 声明非可选字符串变量,值为"sunny"
if let forecast = weatherForecast {  // 对非可选类型进行可选绑定(语法允许但没必要)
    print("The forecast is \(forecast).")  // 打印含天气预报的消息
} else {
    print("No forecast available.")  // 非可选类型不会进入该分支
}

问题 3/12:以下代码会打印一条消息,这种说法是对还是错?

swift
let song: String? = "Shake it Off"  // 声明可选字符串常量,值为"Shake it Off"
if let unwrappedSong = song {  // 可选绑定解包,有值则进入分支
    print("The name has \(unwrappedSong.count) letters.")  // 打印歌曲名的字符数
}

问题 4/12:以下代码会打印一条消息,这种说法是对还是错?

swift
let currentDestination: String? = nil  // 声明可选字符串常量,值为nil
if let destination = currentDestination {  // 可选绑定解包,无值则进入else分支
    print("We're walking to \(destination).")  // 有值时打印的消息
} else {
    print("We're just wandering.")  // 无值时打印的消息
}

问题 5/12:以下代码会打印一条消息,这种说法是对还是错?

swift
let tableHeight: Double? = 100  // 声明可选Double类型常量,值为100
if tableHeight > 85.0 {  // 错误:可选类型不能直接参与比较运算(未解包)
    print("The table is too high.")  // 代码无法编译,不会执行打印
}

问题 6/12:以下代码会打印一条消息,这种说法是对还是错?

swift
let menuItems: [String]? = ["Pizza", "Pasta"]  // 声明可选字符串数组常量,含两个元素
if let items = menuItems {  // 可选绑定解包,将值赋给items(但未使用items)
    print("There are \(menuItems.count) items to choose from.")  // 错误:menuItems仍为可选类型,不能直接调用count
}

问题 7/12:以下代码会打印一条消息,这种说法是对还是错?

swift
var score: Int = nil  // 错误:非可选类型Int不能赋值为nil(编译失败)
score = 556
if let playerScore = score {
    print("You scored \(playerScore) points.")
}

问题 8/12:以下代码会打印一条消息,这种说法是对还是错?

swift
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:以下代码会打印一条消息,这种说法是对还是错?

swift
let userAge: Int? = 38  // 声明可选Int类型常量,值为38
if let age = userAge {  // 可选绑定解包,有值则进入分支
    print("You are \(age) years old.")  // 打印含年龄的消息
}

问题 10/12:以下代码会打印一条消息,这种说法是对还是错?

swift
let favoriteTennisPlayer: String? = "Andy Murray"  // 声明可选字符串常量,值为"Andy Murray"
if player {  // 错误:未定义变量player(应为favoriteTennisPlayer,且可选类型不能直接用于条件判断)
    print("Let's watch \(player)'s highlights video on YouTube.")  // 代码无法编译,不会执行打印
}

问题 11/12:以下代码会打印一条消息,这种说法是对还是错?

swift
var winner: String? = nil  // 声明可选字符串变量,初始值为nil
winner = "Daley Thompson"  // 给变量赋值
if let name = winner {  // 可选绑定解包,有值则进入分支
    print("And the winner is... \(name)!")  // 打印含获胜者姓名的消息
}

问题 12/12:以下代码会打印一条消息,这种说法是对还是错?

swift
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 的用法如下:

swift
func printSquare(of number: Int?) {
    guard let number = number else {
        print("缺少输入值")
        return
    }

    print("\(number) × \(number) = \(number * number)")
}

if let 一样,guard let 会检查可选类型内部是否包含值,如果包含值,就会提取该值并将其存入我们指定的常量中。

不过,二者的执行逻辑恰好相反:

swift
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 就是为这种编程风格设计的,实际上它还提供了两项辅助功能:

  1. 如果你使用 guard 检查函数输入的有效性,Swift 会强制要求你在检查失败时使用 return(退出函数)。
  2. 如果检查通过,且被解包的可选类型内部包含值,那么在 guard 代码块执行结束后,你仍然可以使用这个解包后的值。

查看前面的 printSquare() 函数,就能看到这两项功能的实际应用:

swift
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 的设计初衷是 “如果检查失败,就退出当前函数、循环或条件语句”,因此通过它解包后的值,在检查结束后仍然可以使用。

为了展示二者的差异,我们先定义一个返回 “生命的意义”(可选整数类型)的函数:

swift
func getMeaningOfLife() -> Int? {
    42
}

接下来,在另一个名为 printMeaningOfLife() 的函数中使用它:

swift
func printMeaningOfLife() {
    if let name = getMeaningOfLife() {
        print(name)
    }
}

这里使用了 if let,因此只有当 getMeaningOfLife() 返回整数(而非 nil)时,才会打印结果。

如果用 guard let 改写这段代码,会是这样:

swift
func printMeaningOfLife() {
    guard let name = getMeaningOfLife() else {
        return
    }

    print(name)
}

是的,代码稍微变长了,但有两个重要变化:

  1. 它让我们可以专注于 “理想路径”(happy path)—— 即函数在一切正常时的行为(此处为打印 “生命的意义”)。
  2. guard 强制要求我们在使用它时退出当前作用域,在这个例子中,意味着如果检查失败,我们必须从函数中返回。这是强制性要求:如果不写 return,Swift 会编译报错。

通常会在方法开头使用一次或多次 guard,因为它可以用于预先验证某些条件是否成立。相比 “检查一个条件→执行一段代码→再检查另一个条件→执行另一段代码” 的写法,这种方式能让代码更易读。

因此,如果你只是想解包一些可选类型,就用 if let;但如果是要专门检查条件是否成立(确保后续代码能正常执行),就优先使用 guard let

【练习题】使用 guard 解包

问题 1/12:以下代码会打印消息吗?(正确选 true,错误选 false)

swift
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)

swift
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)

swift
func playScale(name: String?) {
	guard let name = name {  // 注:此处代码存在语法错误,guard 条件后需加 else
		return
	}
	print("正在演奏 \(name) 调音阶。")
}
playScale(name: "C")

问题 4/12:以下代码会打印消息吗?(正确选 true,错误选 false)

swift
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)

swift
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)

swift
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)

swift
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)

swift
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)

swift
func test(number: Int?) {
	guard let number = number else { return }
	print("数字是 \(number)")
}
test(number: 42)  // number 能成功解包,会执行 print 语句

问题 10/12:以下代码会打印消息吗?(正确选 true,错误选 false)

swift
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)

swift
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)

swift
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),我们可以通过它解包可选类型,如果可选类型为空,还能提供一个默认值。

让我们先回顾一下基础内容:

swift
let captains = [
    "Enterprise": "Picard",
    "Voyager": "Janeway",
    "Defiant": "Sisko"
]

let new = captains["Serenity"]

上面的代码从 captains 字典中读取了一个不存在的键,这意味着 new 会是一个被设为 nil 的可选字符串(String?)。

使用空值合并运算符(写法为 ??),我们可以为任何可选类型提供默认值,示例如下:

swift
let new = captains["Serenity"] ?? "N/A"

这段代码会从 captains 字典中读取对应的值并尝试解包。如果可选类型内部包含值,该值会被返回并存储到 new 中;如果可选类型为空(nil),则会使用 “N/A” 作为替代值。

这意味着无论可选类型包含什么 —— 是具体值还是 nil—— 最终结果都是 new 会是一个非可选的普通字符串。这个字符串可能是来自 captains 字典中的对应值,也可能是 “N/A”。

现在,你可能会想:我们难道不能在从字典读取值时直接指定默认值吗?如果你有这样的疑问,那你的想法完全正确:

swift
let new = captains["Serenity", default: "N/A"]

这种写法会产生完全相同的结果,这可能会让人觉得空值合并运算符毫无用处。但实际上,空值合并运算符不仅能用于字典,还能用于任何可选类型

例如,数组的 randomElement() 方法会从数组中返回一个随机元素,但它返回的是可选类型 —— 因为你可能会对空数组调用这个方法。因此,我们可以使用空值合并运算符为其提供默认值:

swift
let tvShows = ["Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"

又或者,你有一个包含可选属性的结构体,并且希望在该属性缺失时提供一个合理的默认值:

swift
struct Book {
    let title: String
    let author: String?
}

let book = Book(title: "Beowulf", author: nil)
let author = book.author ?? "Anonymous"
print(author)

空值合并运算符在将字符串转换为整数时也很实用:这种转换会返回一个可选的 Int?,因为转换可能失败 —— 你提供的字符串可能不是有效的整数(比如 “Hello”)。此时我们可以使用空值合并运算符提供默认值,示例如下:

swift
let input = ""
let number = Int(input) ?? 0
print(number)

由此可见,只要你有一个可选类型,并且希望使用其内部的值(若存在),或在其为空时使用默认值,空值合并运算符就能派上用场。

【可选阅读】在 Swift 中何时应该使用空值合并运算符?

作者:Paul Hudson 2021 年 3 月 11 日 已针对 Xcode 16.4 更新

空值合并运算符允许我们尝试解包可选类型,但如果可选类型包含 nil,则会使用默认值。这在 Swift 中非常实用:虽然可选类型是一项很棒的特性,但通常情况下,使用非可选类型(比如一个确定的字符串,而非 “可能是字符串、也可能是 nil” 的可选字符串)会更方便,而空值合并运算符正是获取非可选类型的绝佳方式。

例如,如果你正在开发一个聊天应用,并且需要加载用户保存的消息草稿,你可能会写出这样的代码:

swift
let savedData = loadSavedMessage() ?? ""

因此,如果 loadSavedMessage() 返回一个包含字符串的可选类型,该字符串会被解包并存储到 savedData 中;但如果这个可选类型是 nil,Swift 会将 savedData 设为空字符串。无论哪种情况,savedData 最终都会是 String 类型,而非 String? 类型。

你也可以链式使用空值合并运算符(尽管这种用法并不常见)。如果你需要,像下面这样的代码是有效的:

swift
let savedData = first() ?? second() ?? ""

这段代码会先尝试调用 first(),如果其返回 nil,则尝试调用 second();如果 second() 也返回 nil,则会使用空字符串作为最终值。

记住,从字典中读取键值对时,返回的结果始终是可选类型。因此,你可能需要在这里使用空值合并运算符,以确保得到一个非可选类型的值:

swift
let scores = ["Picard": 800, "Data": 7000, "Troi": 900]
let crusherScore = scores["Crusher"] ?? 0

不过,这其实是个人偏好问题 —— 字典还提供了一种略有不同的方式,允许我们为 “键不存在” 的情况指定默认值:

swift
let crusherScore = scores["Crusher", default: 0]

你可以选择自己喜欢的方式 —— 从字典中读取值时,这两种写法没有本质区别。

【练习题】空值合并运算符

题目 1/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
let painter: String = "Leonardo da Vinci"
var artist: String = painter ?? "Unknown"

题目 2/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
var bestPony: String? = "Pinkie Pie"
let selectedPony: String? == bestPony ?? nil

题目 3/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
let lightsaberColor: String? = "green"
let color = lightsaberColor ?? "blue"

题目 4/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
var captain: String? = "Kathryn Janeway"
let name = captain ?? "Anonymous"

题目 5/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
let numberSum: Double? = 0.0
let sum: Double = numberSum ??

题目 6/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
var conferenceName: String? = "WWDC"
var conference: String = conferenceName ?? nil

题目 7/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
let planetNumber: Int? = 426
var destination = planetNumber ?? 3

题目 8/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
let userID: Int? = 556
let id = userID ?? "Unknown"

题目 9/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
let distanceRan: Double? = 0.5
let distance: Double = distanceRan ?? 0

题目 10/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
var userOptedIn: Bool? = nil
var optedIn = userOptedIn ?? false

题目 11/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
let jeansNumber: Int? = nil
let jeans = jeansNumber ? 501

题目 12/12:以下代码演示了如何对可选类型使用空值合并运算符 —— 这种说法正确吗?

swift
var selectedYear: Int? = nil
let actualYear = selectedYear ?? 1989

14.4 如何使用可选链处理多个可选值

作者:Paul Hudson 2021 年 10 月 13 日 已针对 Xcode 16.4 更新

可选链(Optional Chaining)是一种简化的语法,用于读取 “可选值内部的可选值”。听起来这可能是一种不常用的功能,但只要看一个例子,你就会明白它的实用性。

先看下面这段代码:

swift
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” 的名字以小写开头,转为大写后才能正确排序)。

对应的代码实现如下:

swift
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

举一个简单的例子:假设我们有一组人名,需要根据他们姓氏的首字母来确定排序位置:

swift
let names = ["Vincent": "van Gogh", "Pablo": "Picasso", "Claude": "Monet"]
let surnameLetter = names["Vincent"]?.first?.uppercased()

在这段代码中,我们两次使用了可选链:

  1. 访问字典值 names["Vincent"] 时使用可选链 —— 因为字典中可能不存在键为 “Vincent” 的条目;
  2. 读取姓氏的首字符时使用可选链 —— 因为字符串有可能是空的。

可选链与空值合并运算符是 “绝佳搭档”:可选链帮我们穿透多层可选值,空值合并运算符则在任意一层可选值为空时提供合理的默认值。

回到上面的姓氏例子,我们可以在无法读取姓氏首字母时,自动返回 “?” 作为默认值:

swift
let surnameLetter = names["Vincent"]?.first?.uppercased() ?? "?"

【练习题】可选链

题目 1/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let names = ["Taylor", "Paul", "Adele"]
let lengthOfLast = names.last?.count?

题目 2/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let credentials = ["twostraws", "fr0sties"]
let lowercaseUsername = credentials.first.lowercased()

题目 3/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let songs: [String]? = [String]()
let finalSong = songs?.last?.uppercased()

题目 4/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

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 代码吗?—— 正确或错误?

swift
let attendees: [String] = [String]()
let firstInLine = attendees?.first?.uppercased()

题目 6/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let shoppingList = ["eggs", "tomatoes", "grapes"]
let firstItem = shoppingList.first?.appending(" are on my shopping list")

题目 7/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let captains: [String]? = ["Archer", "Lorca", "Sisko"]?
let lengthOfBestCaptain = captains.last?.count

题目 8/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
func loadForecast(for dayNumber: Int) -> String {
	print("Forecast unavailable.")
	return nil
}
let forecast = loadForecast(for: 3)?.uppercased()

题目 9/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let capitals = ["Scotland": "Edinburgh", "Wales": "Cardiff"]
let scottishCapital = capitals["Scotland"]?.uppercased()

题目 10/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let favoriteColors = ["Paul": "Red", "Charlotte": "Pink"]
let charlotteColor = favoriteColors["Charlotte"]?.lowercased()

题目 11/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

swift
let opposites = ["hot": "cold", "near": "far"]
let oppositeOfLight = opposites["light"].uppercased()

题目 12/12:以下代码是有效的 Swift 代码吗?—— 正确或错误?

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! 应极少使用。)

不过,还有一种替代方案:如果我们只关心函数是成功还是失败,可以使用可选 trytry?)让函数返回一个可选值。如果函数执行过程中没有抛出任何错误,该可选值会包含函数的返回值;如果抛出了任何错误,函数则会返回 nil。这意味着我们无法知道具体抛出了哪种错误,但很多时候这已经足够 —— 我们可能只关心函数是否执行成功。

具体用法如下:

swift
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 明确你的意图。例如:

swift
let user = (try? getUser(id: 23)) ?? "匿名用户"
print(user)

try? 主要用于以下三种场景:

  1. guard let 结合使用:若 try? 调用返回 nil,则退出当前函数。
  2. 与空值合并运算符结合使用:尝试执行某个操作,失败时提供默认值。
  3. 调用无返回值的抛出函数时,若完全不关心执行结果(例如写入日志文件或向服务器发送统计数据)。

【可选阅读】何时应使用可选 try(try?

作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新

在 Swift 中,我们可以使用 dotrycatch 来执行可能抛出错误的函数,但还有一种替代方式:使用 try? 将抛出函数的调用结果转换为可选值。如果函数执行成功,返回值会是一个包含正常结果的可选值;如果执行失败,返回值则为 nil

使用可选 try 有其优缺点,核心取决于 “错误信息对你的重要程度”。如果你只需要知道函数执行成功或失败(无需区分失败的具体原因),那么可选 try 会非常适用 —— 因为它能将复杂的错误处理简化为 “是否成功” 这一判断。

例如,无需编写如下代码:

swift
do {
    let result = try runRiskyFunction() // 执行有风险的函数
    print(result)
} catch {
    // 执行失败的处理逻辑
}

你可以直接写成:

swift
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 之间的一个随机数。

听起来可能很简单?但我还没说关键要求:这个函数必须用一行代码实现。注意,这不是让你先写多行代码再删除所有换行符 —— 你需要真正能在一行代码内完成整个逻辑。

我稍后会给出一些提示,但建议你先尝试自己解决。

还在看提示吗?好的,以下是一些提示:

  1. 函数的参数类型应为 [Int]?—— 即 “可能存在、也可能为 nil 的整数数组”。
  2. 函数需要返回一个非可选类型Int
  3. 可以使用可选链调用数组的 randomElement() 方法(即使数组是可选类型),该方法的返回值本身也是一个可选类型。
  4. 由于需要返回非可选的整数,你应该使用空值合并运算符(??)来指定 “当数组为 nil 或为空时,返回 1 到 100 之间的随机数”。

本站使用 VitePress 制作