控制流
通过分支、循环和提前退出等方式组织代码。
Swift 提供了多种控制流语句。其中包括 while 循环,用于多次执行一项任务;if、guard 和 switch 语句,根据特定条件执行不同的代码分支;以及 break 和 continue 等语句,用于将执行流转移到代码中的其他位置。Swift 还提供了 for-in 循环,可轻松遍历数组、字典、区间、字符串及其他序列。此外,Swift 提供了 defer 语句,用于封装在离开当前作用域时要执行的代码。
Swift 的 switch 语句比许多类 C 语言中的 switch 语句功能强大得多。case 可以匹配多种不同的模式,包括区间匹配、元组以及转换为特定类型。switch 语句中匹配的值可以绑定到临时常量或变量,以便在 case 主体中使用,并且每个 case 都可以使用 where 子句来表达复杂的匹配条件。
for-in 循环
使用 for-in 循环遍历序列,例如数组中的元素、数字范围或字符串中的字符。
以下示例使用 for-in 循环遍历数组中的元素:
你还可以遍历字典以访问其键值对。当遍历字典时,字典中的每个元素都作为一个 (key, value) 元组返回,并且你可以将 (key, value) 元组的成员分解为显式命名的常量,以便在 for - in 循环体中使用。在下面的代码示例中,字典的键被分解为一个名为 animalName 的常量,字典的值被分解为一个名为 legCount 的常量。
字典遍历顺序
字典的内容本质上是无序的,对其进行遍历并不能保证按特定顺序获取元素。具体而言,你向字典中插入元素的顺序并不能决定遍历它们的顺序。有关数组和字典的更多信息,请参阅“集合类型”。
使用 for-in 循环遍历数值区间
你还可以在数值区间上使用 for-in 循环。以下示例打印了 5 的乘法表的前几项:
正在遍历的序列是一个从 1 到 5(包含 1 和 5)的数字区间,这由闭区间运算符(...)表示。index 的值被设为该区间中的第一个数字(即 1),然后执行循环体内的语句。在这个例子中,循环体里只有一条语句,它会打印出当前 index 值对应的 5 的乘法表中的一项。语句执行完毕后,index 的值会更新为区间中的第二个值(即 2),接着再次调用 print(_:separator:terminator:) 函数。这个过程会一直持续,直到到达区间的末尾。
在上述示例中,index 是一个常量,其值会在每次循环开始时自动设置。因此,index 在使用前无需声明。只需将其包含在循环声明中,它就会被隐式声明,而无需使用 let 声明关键字。
如果你不需要序列中的每个值,可以使用下划线来替代变量名,从而忽略这些值。
上面的示例计算了一个数的另一个数次幂(在这个例子中是 3 的 10 次幂)。它从起始值 1(即 3 的 0 次幂)开始,将其乘以 3,共乘 10 次,使用的是一个从 1 到 10 的闭区间。对于这个计算,每次循环时具体的计数器值是不需要的 —— 代码只是让循环正确执行指定的次数。用下划线字符(_)代替循环变量可以忽略这些具体的值,并且在每次循环迭代期间不会访问当前值。
在某些情况下,你可能不想使用包含两个端点的闭区间。以在手表表盘上绘制每分钟的刻度线为例。你要绘制 60 条刻度线,从 0 分钟开始。这时可以使用半开区间运算符(..<),它包含下限但不包含上限。有关区间的更多信息,请参阅“区间运算符”。
有些用户可能希望他们的用户界面(UI)中有更少的刻度线。他们可能更倾向于每 5 分钟设置一个刻度。可以使用 stride(from:to:by:) 函数来跳过不需要的刻度。
通过使用 stride(from:through:by:) 函数,也可以实现闭区间的步长遍历:
上述示例使用 for-in 循环来遍历区间、数组、字典和字符串。不过,只要类型符合 Sequence 协议,你就可以使用这种语法来遍历任何集合,包括你自己定义的类和集合类型。
while 循环
while 循环会持续执行一组语句,直到某个条件变为 false。当在第一次迭代开始之前还不知道迭代次数时,使用这类循环是最合适的。Swift 提供了两种 while 循环:
while:在每次循环开始时计算条件。repeat-while:在每次循环结束时计算条件。
while
while 循环首先计算一个条件。如果条件为 true,就会重复执行一组语句,直到条件变为 false。
以下是 while 循环的一般形式:
repeat - while 循环
while 循环的另一种变体,即 repeat - while 循环,会先执行一次循环体,再去判断循环条件。然后它会持续重复执行循环,直到条件为 false。
注意
Swift 中的 repeat - while 循环类似于其他语言中的 do - while 循环。
以下是 repeat - while 循环的一般形式:
条件语句
根据特定条件执行不同的代码段通常很有用。比如,当出现错误时你可能想运行一段额外的代码,或者当某个值过高或过低时显示一条消息。要实现这一点,你需要让代码的某些部分具有条件性。
Swift 提供了两种向代码中添加条件分支的方式:if 语句和 switch 语句。通常,你会使用 if 语句来评估只有几种可能结果的简单条件。而 switch 语句更适合有多种可能排列组合的复杂条件,并且在模式匹配有助于选择合适的代码分支来执行的情况下很有用。
if
if 语句最简单的形式是只有一个 if 条件。只有当该条件为 true 时,它才会执行一组语句。
在这里,每个分支都会为 weatherAdvice 常量设置一个值,该常量会在 if 语句结束后被打印出来。
使用另一种语法,也就是所谓的 if 表达式,你可以更简洁地编写这段代码:
在这个 if 表达式版本中,每个分支都包含一个单一的值。如果某个分支的条件为 true,那么该分支的值就会在 weatherAdvice 的赋值操作中作为整个 if 表达式的值。每个 if 分支都有对应的 else if 分支或 else 分支,这确保了总有一个分支会匹配成功,并且无论哪些条件为 true,if 表达式总能产生一个值。
由于赋值操作的语法在 if 表达式外部就已开始,因此无需在每个分支内重复 weatherAdvice =。相反,if 表达式的每个分支都会产生 weatherAdvice 三种可能值中的一个,然后赋值操作会使用这个值。
if 表达式的所有分支都需要包含相同类型的值。因为 Swift 会分别检查每个分支的类型,像 nil 这种可用于多种类型的值会阻碍 Swift 自动确定 if 表达式的类型。此时,你需要显式指定类型,例如:
在上述代码中,if 表达式的一个分支具有字符串值,而另一个分支具有 nil 值。nil 值可作为任何可选类型的值使用,因此你必须像“类型注解”中所描述的那样,显式声明 freezeWarning 是一个可选字符串。
提供此类型信息的另一种方式是为 nil 显式指定类型,而不是为 freezeWarning 显式指定类型:
if 表达式可以通过抛出错误,或者调用像 fatalError(_:file:line:) 这种不会有返回值的函数,来应对意外的失败情况。例如:
在这个例子中,if 表达式会检查预报温度是否高于 100°C(水的沸点)。如果温度达到这么高,if 表达式会抛出一个 .boiling 错误,而不是返回一个文本摘要。尽管这个 if 表达式可能会抛出错误,但在它前面不需要写 try。有关处理错误的信息,请参阅“错误处理”。
除了像上述示例那样在赋值语句的右侧使用 if 表达式,你还可以将其用作函数或闭包返回的值。
switch 语句
switch 语句会考量一个值,并将其与多个可能匹配的模式进行比较。然后,根据第一个成功匹配的模式,执行相应的代码块。switch 语句为响应多种可能状态提供了一种替代 if 语句的方式。
switch 语句最简单的形式是将一个值与一个或多个相同类型的值进行比较。
每个 switch 语句都由多个可能的 case(分支)组成,每个分支都以 case 关键字开头。除了与特定值进行比较之外,Swift 还提供了多种方式让每个 case 指定更复杂的匹配模式。本章后续会介绍这些选项。
和 if 语句体一样,每个 case 都是一个独立的代码执行分支。switch 语句会确定应该选择哪个分支。这个过程被称为对所考量的值进行“分支选择”。
每个 switch 语句都必须是详尽的。也就是说,所考量类型的每个可能的值都必须能被某个 switch 分支匹配到。如果为每个可能的值都提供一个 case 不合适,你可以定义一个默认分支来涵盖那些未被明确处理的值。这个默认分支用 default 关键字表示,并且必须总是放在最后。
这个示例使用 switch 语句来考量一个名为 someCharacter 的单个小写字符:
在这个例子中,
switch 表达式里的每个 case 都包含了 message 的值,当该 case 与 anotherCharacter 匹配时就会使用这个值。由于 switch 语句总是要求详尽匹配,所以总会有一个值用于赋值。
和 if 表达式一样,你可以抛出一个错误,或者调用像 fatalError(_:file:line:) 这种不会有返回值的函数,而不是为某个特定的 case 提供一个值。你可以像上述示例那样,在赋值语句的右侧使用 switch 表达式,也可以将其作为函数或闭包返回的值。
无隐式贯穿
与 C 和 Objective - C 中的 switch 语句不同,Swift 中的 switch 语句默认不会从每个 case 的末尾贯穿到下一个 case。相反,一旦第一个匹配的 switch 分支执行完毕,整个 switch 语句就会结束执行,无需显式的 break 语句。这使得 Swift 的 switch 语句比 C 语言中的更安全、更易用,也避免了误执行多个 switch 分支的情况。
注意
虽然在 Swift 中 break 不是必需的,但你可以使用 break 语句来匹配并忽略某个特定的 case,或者在某个匹配的 case 执行完毕之前跳出该 case。详细信息请参阅“switch 语句中的 break”。
每个 case 的语句体必须至少包含一条可执行语句。以下代码是无效的,因为第一个 case 为空:
与 C 语言中的 switch 语句不同,这个 switch 语句不会同时匹配 "a" 和 "A"。相反,它会报出一个编译时错误,提示 case "a": 不包含任何可执行语句。这种处理方式避免了意外地从一个 case 贯穿到另一个 case,使代码更安全,意图也更清晰。
若要使用单个 case 同时匹配 "a" 和 "A",可以将这两个值组合成一个复合 case,用逗号分隔这些值。
为了提高可读性,复合 case 也可以写成多行形式。有关复合 case 的更多信息,请参阅“复合 case”。
注意
如果要在某个特定的 switch case 结束时显式地贯穿到下一个 case,请使用 fallthrough 关键字,详情请参阅“贯穿”。
区间匹配
可以检查 switch case 中的值是否包含在某个区间内。以下示例使用数字区间为任意大小的数字提供自然语言描述的数量表达:
在上述示例中,approximateCount 在 switch 语句中进行判断。每个 case 都会将该值与一个数字或区间进行比较。由于 approximateCount 的值介于 12 和 100 之间,naturalCount 被赋值为 “dozens of”,随后程序执行流程会跳出 switch 语句。
元组
你可以使用元组在同一个 switch 语句中测试多个值。元组的每个元素都可以与不同的值或值的区间进行比较。或者,使用下划线字符(_),也称为通配符模式,来匹配任何可能的值。
下面的示例使用一个表示为简单 (Int, Int) 类型元组的 (x, y) 坐标点,并按照示例后面的图表对其进行分类。
switch 语句判断该点是在原点 (0, 0) 、在红色的 x 轴上、在绿色的 y 轴上、在以原点为中心的蓝色 4x4 方格内,还是在方格之外。
与 C 语言不同,Swift 允许多个 switch 分支考虑相同的值。实际上,点 (0, 0) 可能匹配此示例中的所有四个分支。但是,如果存在多个可能的匹配项,始终会使用第一个匹配的分支。点 (0, 0) 会首先匹配 case (0, 0),因此所有其他匹配分支将被忽略。
值绑定
switch 分支可以将其匹配的值命名为临时常量或变量,以便在分支体内使用。这种行为称为值绑定,因为这些值在分支体内绑定到临时常量或变量。
下面的示例使用一个表示为 (Int, Int) 类型元组的 (x, y) 点,并按照随后的图表对其进行分类:
where 子句
switch 分支可以使用 where 子句来检查更多条件。
下面的示例对以下图表中的 (x, y) 点进行分类:
switch 语句判断该点是在绿色对角线上(此时x == y),在紫色对角线上(此时x == -y),还是不在这两条对角线上。
这三个 switch 分支声明了占位常量x和y,它们临时获取yetAnotherPoint中的两个元组值。这些常量用作where子句的一部分,以创建动态筛选条件。只有当where子句的条件对该值求值为true时,switch分支才会匹配当前的point值。
和上一个示例一样,最后一个分支匹配所有可能剩余的值,所以不需要default分支来使switch语句详尽无遗。
复合分支
多个拥有相同语句体的switch分支可以合并,方法是在case后编写多个模式,各模式之间用逗号分隔。只要其中任何一个模式匹配,就认为该分支匹配。如果模式列表很长,可以将模式写成多行。例如:
这个switch语句的第一个分支匹配英语中的所有五个小写元音。同样,第二个分支匹配所有小写英语辅音。最后,默认分支匹配任何其他字符。
复合分支也可以包含值绑定。复合分支的所有模式都必须包含相同的一组值绑定,并且每个绑定都必须从复合分支的所有模式中获取相同类型的值。这确保了无论复合分支的哪一部分匹配,分支主体中的代码始终可以访问绑定的值,并且该值始终具有相同的类型。
上述示例有两种模式:(let distance, 0) 匹配 x 轴上的点,而 (0, let distance) 匹配 y 轴上的点。这两种模式都包含对 distance 的绑定,并且在这两种模式中 distance 都是整数 —— 这意味着 case 主体中的代码始终可以访问 distance 的值。
控制转移语句
控制转移语句通过将控制权从一段代码转移到另一段代码,改变代码的执行顺序。Swift 有五种控制转移语句:
continuebreakfallthroughreturnthrow
continue、break 和 fallthrough 语句将在下面介绍。return 语句在 “函数” 部分介绍,throw 语句在 “使用抛出函数传播错误” 部分介绍。
continue
continue 语句告诉循环停止当前正在做的事情,并从循环的下一次迭代开始重新执行。它表示 “我已完成当前的循环迭代”,但不会完全离开循环。
下面的示例从一个小写字符串中移除所有元音字母和空格,以创建一个神秘的谜题短语:
上述代码只要匹配到元音字母或空格,就会调用continue关键字,这会使循环的当前迭代立即结束,并直接跳转到下一次迭代的开始。
break
break语句会立即终止整个控制流语句的执行。当你希望比正常情况更早地终止switch或循环语句的执行时,可在switch或循环语句内部使用break语句。
循环语句中的break
在循环语句内部使用时,break会立即结束循环的执行,并将控制权转移到循环结束大括号(})之后的代码。循环当前迭代中剩余的代码不会被执行,并且不会再启动循环的后续迭代。
switch语句中的break
在switch语句内部使用时,break会使switch语句立即结束执行,并将控制权转移到switch语句结束大括号(})之后的代码。
这种行为可用于在switch语句中匹配并忽略一个或多个case。由于Swift的switch语句要求详尽且不允许有空的case,所以有时为了明确表达意图,有必要刻意匹配并忽略某个case。你可以通过将break语句作为要忽略的case的整个主体来实现这一点。当switch语句匹配到该case时,case内部的break语句会立即结束switch语句的执行。
注意
只包含注释的switch case会报告为编译时错误。注释不是语句,不会使switch case被忽略。始终使用break语句来忽略switch case。
以下示例对Character值进行switch判断,并确定它是否代表四种语言之一中的数字符号。为简洁起见,单个switch case涵盖了多个值。
贯穿(fallthrough)
在Swift中,switch语句不会从一个case的末尾自然贯穿到下一个case。也就是说,一旦第一个匹配的case执行完毕,整个switch语句就完成了执行。相比之下,C语言要求你在每个switch case的末尾插入一个显式的break语句,以防止贯穿行为。Swift避免了默认的贯穿行为,这使得switch语句比C语言中的同类语句更加简洁和可预测,从而避免了意外执行多个switch case的情况。
如果你需要C语言风格的贯穿行为,可以在每个case的基础上,使用fallthrough关键字来选择启用这种行为。下面的示例使用fallthrough来创建一个数字的文本描述。
这个示例声明了一个名为description的新String变量,并为其赋予初始值。然后,函数使用switch语句来判断integerToDescribe的值。如果integerToDescribe的值是列表中的质数之一,函数会在description的末尾追加文本,以表明该数字是质数。接着,它使用fallthrough关键字 “贯穿” 到default分支。default分支会在description的末尾添加一些额外的文本,至此switch语句执行完毕。
除非integerToDescribe的值在已知质数列表中,否则它根本不会与第一个switch case匹配。由于没有其他特定的case,integerToDescribe会与default分支匹配。
在switch语句执行完毕后,使用print(_:separator:terminator:)函数打印该数字的描述。在这个示例中,数字5被正确识别为质数。
注意
fallthrough关键字不会检查它导致执行转入的switch case的条件。fallthrough关键字只是使代码执行直接转移到下一个case(或default分支)块内的语句,就像C语言标准switch语句的行为一样。即使不满足下一个 case判断条件也能进入 case 语句中
带标签语句
在Swift中,你可以将循环和条件语句嵌套在其他循环和条件语句内部,以创建复杂的控制流结构。然而,循环和条件语句都可以使用break语句提前结束其执行。因此,有时明确指出你希望break语句终止哪个循环或条件语句是很有用的。同样,如果你有多个嵌套循环,明确指出continue语句应影响哪个循环也很有用。
为了实现这些目的,你可以用语句标签标记循环语句或条件语句。对于条件语句,你可以将语句标签与break语句配合使用,以结束带标签语句的执行。对于循环语句,你可以将语句标签与break或continue语句配合使用,以结束或继续带标签语句的执行。
带标签语句通过将标签与语句的起始关键字放在同一行,后面紧跟一个冒号来表示。以下是while循环的这种语法示例,不过所有循环和switch语句的原理都是一样的:
这个版本的游戏使用
while循环和switch语句来实现游戏逻辑。while循环带有一个名为gameLoop的语句标签,用以表明它是“蛇梯棋”游戏的主游戏循环。
while循环的条件是while square!= finalSquare,这意味着玩家必须正好落在第25格上。
在每次循环开始时掷骰子。循环并不会立即移动玩家位置,而是使用switch语句来考量移动结果,并判断该移动是否被允许:
- 如果掷出的骰子点数会使玩家移动到最终方格,游戏结束。
break gameLoop语句将控制权转移到while循环外部的第一行代码,从而结束游戏。 - 如果掷出的骰子点数会使玩家移动超过最终方格,该移动无效,玩家需要重新掷骰子。
continue gameLoop语句结束当前while循环迭代,并开始下一次循环迭代。 - 在其他所有情况下,掷出的骰子点数对应的移动是有效的。玩家向前移动
diceRoll个方格,然后游戏逻辑检查是否遇到蛇梯。接着循环结束,控制权回到while条件判断处,以决定是否需要进行下一回合。
注意
如果上述break语句没有使用gameLoop标签,它将跳出switch语句,而不是while语句。使用gameLoop标签能明确表明应该终止哪个控制语句。
在调用continue gameLoop跳转到循环的下一次迭代时,严格来说并不一定非要使用gameLoop标签。因为游戏中只有一个循环,所以continue语句会影响哪个循环并无歧义。不过,在continue语句中使用gameLoop标签并无坏处。这样做与在break语句中使用该标签保持一致,有助于使游戏逻辑更易于阅读和理解。
提前退出
guard语句和if语句类似,会根据表达式的布尔值来执行语句。使用guard语句是为了要求某个条件必须为真,这样guard语句之后的代码才能执行。与if语句不同,guard语句始终有一个else子句 —— 如果条件不为真,就会执行else子句中的代码。
如果
guard语句的条件满足,代码执行会在guard语句的结束花括号之后继续。在条件中通过可选绑定赋值的任何变量或常量,在guard语句所在的代码块的其余部分都可用。
如果条件不满足,则会执行else分支内的代码。该分支必须转移控制权,以退出guard语句所在的代码块。它可以通过诸如return、break、continue或throw之类的控制转移语句来实现,或者可以调用一个不返回的函数或方法,例如fatalError(_:file:line:)。
与使用if语句进行相同的检查相比,使用guard语句来处理必要条件可提高代码的可读性。它使你可以编写通常会执行的代码,而无需将其包装在else块中,并且可以将处理未满足条件的代码放在该条件旁边。
延迟执行操作
与if和while等控制流结构不同,if和while用于控制代码的某部分是否执行或执行多少次,而defer用于控制一段代码何时执行。你可以使用defer块编写稍后执行的代码,当程序到达当前作用域的末尾时,就会执行这些代码。例如:
在上述示例中,defer块内的代码会在退出if语句主体之前执行。首先,if语句中的代码运行,将score增加5。然后,在退出if语句的作用域之前,运行延迟执行的代码,即打印score。
无论程序如何退出该作用域,defer内的代码始终会运行。这包括从函数提前返回、跳出for循环或抛出错误等情况。这种特性使得defer对于需要确保成对操作执行的场景很有用,比如手动分配和释放内存、打开和关闭底层文件描述符,以及在数据库中开始和结束事务,因为你可以在代码中将这两个操作写在一起。例如,以下代码通过在一段代码内对score先加100再减100,给score一个临时奖励:
如果在同一作用域内编写多个 defer 块,你最先指定的 defer 块会最后运行。
如果程序停止运行(例如,由于运行时错误或崩溃),延迟执行的代码将不会执行。但是,在抛出错误后,延迟执行的代码会执行;有关在错误处理中使用defer的信息,请参阅“指定清理操作”。
检查API可用性
Swift 内置了对API可用性检查的支持,这能确保你不会意外使用在特定部署目标上不可用的API。
编译器会利用SDK中的可用性信息,来验证代码中使用的所有API在项目指定的部署目标上是否可用。如果你尝试使用不可用的API,Swift会在编译时报告错误。
你可以在if或guard语句中使用可用性条件,根据所需使用的API在运行时是否可用,有条件地执行一段代码。编译器在验证该代码块中的API是否可用时,会使用来自可用性条件的信息。
上述可用性条件指定,在iOS系统中,if语句的主体仅在iOS 10及更高版本中执行;在macOS系统中,仅在macOS 10.12及更高版本中执行。最后一个参数*是必需的,它指定在任何其他平台上,if语句的主体将在项目目标所指定的最低部署目标上执行。
一般来说,可用性条件采用平台名称和版本号的列表形式。你可以使用诸如iOS、macOS、watchOS、tvOS和visionOS等平台名称 —— 完整列表请参阅“声明属性”。除了指定像iOS 8或macOS 10.10这样的主版本号,你还可以指定像iOS 11.2.6和macOS 10.13.3这样的次版本号。
当你在guard语句中使用可用性条件时,它会完善该代码块中其余代码所使用的可用性信息。
在上述示例中,
ColorPreference结构体要求在macOS 10.12或更高版本下使用。chooseBestColor()函数以一个可用性guard语句开头。如果平台版本过低,无法使用ColorPreference,函数会回退到始终可用的行为。在guard语句之后,你可以使用要求macOS 10.12或更高版本的API。
除了#available,Swift还支持使用不可用性条件进行相反的检查。例如,以下两个检查的作用相同:
当检查仅包含备用代码时,使用
#unavailable形式有助于提高代码的可读性。