参考资料

控制流

Swift 提供了多种控制流语句。包括用于多次执行任务的 while 循环;用于根据特定条件执行不同代码分支的 if、guard 和 switch 语句;以及用于将执行流程转移到代码其他位置的 break 和 continue 等语句。Swift 提供的 for-in 循环可以轻松遍历数组、字典、区间、字符串和其他序列。Swift 还提供了 defer 语句,用于包装在离开当前作用域时执行的代码。

控制流

Swift 提供了多种控制流语句。包括用于多次执行任务的 while 循环;用于根据特定条件执行不同代码分支的 ifguardswitch 语句;以及用于将执行流程转移到代码其他位置的 breakcontinue 等语句。Swift 提供的 for-in 循环可以轻松遍历数组、字典、区间、字符串和其他序列。Swift 还提供了 defer 语句,用于包装在离开当前作用域时执行的代码。

Swift 的 switch 语句比许多类 C 语言中的对应语句强大得多。case 可以匹配多种不同的模式,包括区间匹配、元组和特定类型的转换。switch case 中匹配的值可以绑定到临时常量或变量,以便在 case 主体中使用,复杂的匹配条件可以通过每个 case 的 where 子句来表达。

For-In 循环

使用 for-in 循环可以遍历序列,例如数组中的元素、数字区间或字符串中的字符。

以下示例使用 for-in 循环遍历数组中的元素:

let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
    print("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!

你也可以遍历字典来访问其键值对。遍历字典时,每个元素都以 (key, value) 元组的形式返回,你可以将 (key, value) 元组的成员分解为显式命名的常量,以便在 for-in 循环体中使用。在下面的代码示例中,字典的键被分解为名为 animalName 的常量,字典的值被分解为名为 legCount 的常量。

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
    print("\(animalName)s have \(legCount) legs")
}
// cats have 4 legs
// ants have 6 legs
// spiders have 8 legs

Dictionary 的内容本质上是无序的,遍历它们并不保证检索的顺序。特别是,向 Dictionary 中插入元素的顺序并不决定它们被遍历的顺序。有关数组和字典的更多信息,请参阅集合类型

你也可以对数字区间使用 for-in 循环。以下示例打印五的乘法表的前几项:

for index in 1...5 {
    print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25

被遍历的序列是从 15 的数字区间(包含两端),这是通过闭区间运算符(...)表示的。index 的值首先被设置为区间中的第一个数字(1),然后执行循环体内的语句。在这种情况下,循环只包含一条语句,它打印当前 index 值对应的五的乘法表条目。语句执行后,index 的值更新为区间中的第二个值(2),然后再次调用 print(_:separator:terminator:) 函数。这个过程一直持续到到达区间末尾。

在上面的示例中,index 是一个常量,其值在循环的每次迭代开始时自动设置。因此,index 不需要在使用前声明。它通过包含在循环声明中而被隐式声明,无需使用 let 声明关键字。

如果你不需要序列中的每个值,可以使用下划线代替变量名来忽略这些值。

let base = 3
let power = 10
var answer = 1
for _ in 1...power {
    answer *= base
}
print("\(base) to the power of \(power) is \(answer)")
// Prints "3 to the power of 10 is 59049".

上面的示例计算一个数的幂次方(在本例中是 310 次方)。它使用从 1 开始到 10 结束的闭区间,将起始值 1(即 30 次方)乘以 3,共进行十次。对于这个计算,每次循环的计数器值是不必要的——代码只需要执行正确的循环次数。使用下划线字符(_)代替循环变量会导致各个值被忽略,并且在循环的每次迭代期间不提供对当前值的访问。

在某些情况下,你可能不想使用包含两个端点的闭区间。考虑在表盘上为每分钟画刻度线。你想画 60 个刻度线,从第 0 分钟开始。使用半开区间运算符(..<)来包含下界但不包含上界。有关区间的更多信息,请参阅区间运算符

let minutes = 60
for tickMark in 0..<minutes {
    // render the tick mark each minute (60 times)
}

一些用户可能希望在其 UI 中使用更少的刻度线。他们可能更喜欢每 5 分钟一个刻度。使用 stride(from:to:by:) 函数跳过不需要的刻度。

let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
    // render the tick mark every 5 minutes (0, 5, 10, 15 ... 45, 50, 55)
}

也可以使用闭区间,改用 stride(from:through:by:)

let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
    // render the tick mark every 3 hours (3, 6, 9, 12)
}

以上示例使用 for-in 循环遍历区间、数组、字典和字符串。但是,只要类型遵循 Sequence 协议,你就可以使用这种语法遍历任何集合,包括你自己的类和集合类型。

While 循环

while 循环执行一组语句,直到条件变为 false。这类循环最适合在第一次迭代开始前迭代次数未知的情况下使用。Swift 提供两种 while 循环:

  • while 在每次循环开始时评估其条件。
  • repeat-while 在每次循环结束时评估其条件。

While

while 循环首先评估单个条件。如果条件为 true,则重复执行一组语句,直到条件变为 false

以下是 while 循环的一般形式:

while <#condition#> {
   <#statements#>
}

以下示例演示一个简单的蛇梯棋游戏(也称为滑梯与梯子):

snakesAndLadders

游戏规则如下:

  • 棋盘有 25 个方格,目标是到达或超过第 25 格。
  • 玩家的起始方格是”第零格”,位于棋盘左下角外侧。
  • 每个回合,你掷一个六面骰子,按照上图虚线箭头所示的水平路径移动相应的格数。
  • 如果你的回合结束在梯子底部,你将沿梯子向上移动。
  • 如果你的回合结束在蛇头,你将沿蛇向下移动。

游戏棋盘由 Int 值数组表示。其大小基于名为 finalSquare 的常量,该常量用于初始化数组以及稍后检查获胜条件。由于玩家从棋盘外的”第零格”开始,棋盘初始化为 26 个零 Int 值,而不是 25 个。

let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)

然后为蛇和梯子设置一些特定的方格值。有梯子底部的方格具有正数值让你向上移动,而有蛇头的方格具有负数值让你向下移动。

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

第 3 格包含梯子底部,可以让你移动到第 11 格。为了表示这一点,board[03] 等于 +08,相当于整数值 8311 之间的差值)。为了对齐值和语句,显式使用一元加运算符(+i)与一元减运算符(-i),并且小于 10 的数字用零填充。(这两种风格技巧都不是严格必需的,但它们使代码更整洁。)

var square = 0
var diceRoll = 0
while square < finalSquare {
    // roll the dice
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    // move by the rolled amount
    square += diceRoll
    if square < board.count {
        // if we're still on the board, move up or down for a snake or a ladder
        square += board[square]
    }
}
print("Game over!")

上面的示例使用了一种非常简单的掷骰子方法。它不是生成随机数,而是从 diceRoll0 开始。每次循环时,diceRoll 递增 1,然后检查它是否变得太大。每当这个返回值等于 7 时,骰子值太大了,被重置为 1。结果是 diceRoll 值的序列始终是 12345612,以此类推。

掷骰子后,玩家向前移动 diceRoll 个方格。骰子点数可能使玩家超过第 25 格,在这种情况下游戏结束。为了处理这种情况,代码检查 square 是否小于 board 数组的 count 属性。如果 square 有效,则将存储在 board[square] 中的值添加到当前 square 值,以便玩家沿梯子或蛇上下移动。

注意 如果不执行此检查,board[square] 可能会尝试访问 board 数组边界之外的值,这将触发运行时错误。

当前 while 循环执行结束,然后检查循环条件以确定是否应再次执行循环。如果玩家已移动到或超过第 25 格,循环条件评估为 false,游戏结束。

在这种情况下使用 while 循环是合适的,因为在 while 循环开始时游戏长度不明确。相反,循环会一直执行直到满足特定条件。

Repeat-While

while 循环的另一种变体是 repeat-while 循环,它在考虑循环条件之前首先执行一次循环体。然后继续重复循环直到条件为 false

注意 Swift 中的 repeat-while 循环类似于其他语言中的 do-while 循环。

以下是 repeat-while 循环的一般形式:

repeat {
   <#statements#>
} while <#condition#>

下面再次是蛇梯棋示例,这次使用 repeat-while 循环而不是 while 循环。finalSquareboardsquarediceRoll 的值初始化方式与 while 循环完全相同。

let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0

在这个版本的游戏中,循环中的第一个操作是检查梯子或蛇。棋盘上没有梯子直接将玩家带到第 25 格,因此不可能通过爬梯子赢得游戏。因此,将检查蛇或梯子作为循环中的第一个操作是安全的。

游戏开始时,玩家在”第零格”。board[0] 始终等于 0,没有任何效果。

repeat {
    // move up or down for a snake or ladder
    square += board[square]
    // roll the dice
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    // move by the rolled amount
    square += diceRoll
} while square < finalSquare
print("Game over!")

代码检查蛇和梯子后,掷骰子,玩家向前移动 diceRoll 个方格。当前循环执行结束。

循环条件(while square < finalSquare)与之前相同,但这次它直到循环第一次运行结束后才会被评估。repeat-while 循环的结构比前一个示例中的 while 循环更适合这个游戏。在上面的 repeat-while 循环中,square += board[square] 总是在循环的 while 条件确认 square 仍在棋盘上之后立即执行。这种行为消除了前面描述的 while 循环版本中所需的数组边界检查。

条件语句

根据特定条件执行不同的代码片段通常很有用。你可能希望在发生错误时运行额外的代码,或者在值变得过高或过低时显示消息。要做到这一点,你需要使部分代码成为条件的。

Swift 提供了两种向代码添加条件分支的方法:if 语句和 switch 语句。通常,你使用 if 语句来评估只有少数可能结果的简单条件。switch 语句更适合具有多种可能排列的更复杂条件,在模式匹配可以帮助选择适当代码分支执行的情况下很有用。

If

在最简单的形式中,if 语句有一个 if 条件。只有当该条件为 true 时,它才执行一组语句。

var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
}
// Prints "It's very cold. Consider wearing a scarf."

上面的示例检查温度是否小于或等于 32 华氏度(水的冰点)。如果是,则打印一条消息。否则,不打印任何消息,代码在 if 语句的右大括号后继续执行。

if 语句可以为 if 条件为 false 的情况提供一组替代语句,称为 else 子句。这些语句由 else 关键字指示。

temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
} else {
    print("It's not that cold. Wear a T-shirt.")
}
// Prints "It's not that cold. Wear a T-shirt."

这两个分支总是执行其中一个。因为温度已升至 40 华氏度,不再冷到需要建议戴围巾,所以触发了 else 分支。

你可以将多个 if 语句链接在一起以考虑更多的子句。

temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
    print("It's really warm. Don't forget to wear sunscreen.")
} else {
    print("It's not that cold. Wear a T-shirt.")
}
// Prints "It's really warm. Don't forget to wear sunscreen."

这里添加了一个额外的 if 语句来响应特别温暖的温度。最后的 else 子句保留,它打印对任何既不太暖也不太冷的温度的响应。

但是,最后的 else 子句是可选的,如果条件集不需要完整,可以排除它。

temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
    print("It's really warm. Don't forget to wear sunscreen.")
}

因为温度既不够冷触发 if 条件,也不够暖触发 else if 条件,所以没有打印任何消息。

Swift 提供了 if 的简写拼写,可以在设置值时使用。例如,考虑以下代码:

let temperatureInCelsius = 25
let weatherAdvice: String

if temperatureInCelsius <= 0 {
    weatherAdvice = "It's very cold. Consider wearing a scarf."
} else if temperatureInCelsius >= 30 {
    weatherAdvice = "It's really warm. Don't forget to wear sunscreen."
} else {
    weatherAdvice = "It's not that cold. Wear a T-shirt."
}

print(weatherAdvice)
// Prints "It's not that cold. Wear a T-shirt."

这里,每个分支都为 weatherAdvice 常量设置一个值,该值在 if 语句之后打印。

使用替代语法(称为 if 表达式),你可以更简洁地编写此代码:

let weatherAdvice = if temperatureInCelsius <= 0 {
    "It's very cold. Consider wearing a scarf."
} else if temperatureInCelsius >= 30 {
    "It's really warm. Don't forget to wear sunscreen."
} else {
    "It's not that cold. Wear a T-shirt."
}

print(weatherAdvice)
// Prints "It's not that cold. Wear a T-shirt."

在这个 if 表达式版本中,每个分支包含一个值。如果分支的条件为真,则该分支的值用作整个 if 表达式在 weatherAdvice 赋值中的值。每个 if 分支都有相应的 else if 分支或 else 分支,确保总有一个分支匹配,并且 if 表达式始终产生一个值,无论哪个条件为真。

因为赋值语法从 if 表达式外部开始,所以不需要在每个分支内重复 weatherAdvice =。相反,if 表达式的每个分支产生 weatherAdvice 的三个可能值之一,赋值使用该值。

if 表达式的所有分支都需要包含相同类型的值。因为 Swift 单独检查每个分支的类型,像 nil 这样可以与多种类型一起使用的值会阻止 Swift 自动确定 if 表达式的类型。相反,你需要显式指定类型——例如:

let freezeWarning: String? = if temperatureInCelsius <= 0 {
    "It's below freezing. Watch for ice!"
} else {
    nil
}

在上面的代码中,if 表达式的一个分支有字符串值,另一个分支有 nil 值。nil 值可以用作任何可选类型的值,所以你必须显式写出 freezeWarning 是可选字符串,如类型注解中所述。

提供此类型信息的另一种方法是为 nil 提供显式类型,而不是为 freezeWarning 提供显式类型:

let freezeWarning = if temperatureInCelsius <= 0 {
    "It's below freezing. Watch for ice!"
} else {
    nil as String?
}

if 表达式可以通过抛出错误或调用永不返回的函数(如 fatalError(_:file:line:))来响应意外失败。例如:

let weatherAdvice = if temperatureInCelsius > 100 {
    throw TemperatureError.boiling
} else {
    "It's a reasonable temperature."
}

在这个示例中,if 表达式检查预测温度是否高于 100°C——水的沸点。这么高的温度会导致 if 表达式抛出 .boiling 错误,而不是返回文本摘要。即使这个 if 表达式可以抛出错误,你也不需要在它之前写 try。有关处理错误的信息,请参阅错误处理

除了在赋值的右侧使用 if 表达式(如上面的示例所示),你还可以将它们用作函数或闭包返回的值。

Switch

switch 语句考虑一个值,并将其与几个可能的匹配模式进行比较。然后根据第一个成功匹配的模式执行适当的代码块。switch 语句为响应多种可能状态提供了 if 语句的替代方案。

在最简单的形式中,switch 语句将一个值与一个或多个相同类型的值进行比较。

switch <#some value to consider#> {
case <#value 1#>:
    <#respond to value 1#>
case <#value 2#>,
    <#value 3#>:
    <#respond to value 2 or 3#>
default:
    <#otherwise, do something else#>
}

每个 switch 语句由多个可能的 case 组成,每个 case 以 case 关键字开头。除了与特定值进行比较外,Swift 还为每个 case 提供了几种指定更复杂匹配模式的方法。这些选项将在本章后面描述。

if 语句的主体一样,每个 case 是代码执行的单独分支。switch 语句决定应该选择哪个分支。这个过程称为在被考虑的值上切换

每个 switch 语句必须是穷尽的。也就是说,被考虑类型的每个可能值都必须被某个 switch case 匹配。如果不适合为每个可能的值提供一个 case,你可以定义一个默认 case 来覆盖任何未明确处理的值。这个默认 case 由 default 关键字表示,必须始终出现在最后。

此示例使用 switch 语句来考虑名为 someCharacter 的单个小写字符:

let someCharacter: Character = "z"
switch someCharacter {
case "a":
    print("The first letter of the Latin alphabet")
case "z":
    print("The last letter of the Latin alphabet")
default:
    print("Some other character")
}
// Prints "The last letter of the Latin alphabet".

switch 语句的第一个 case 匹配英语字母表的第一个字母 a,第二个 case 匹配最后一个字母 z。因为 switch 必须有一个 case 对应每个可能的字符,而不仅仅是每个字母字符,所以这个 switch 语句使用 default case 来匹配除 az 之外的所有字符。这个规定确保了 switch 语句是穷尽的。

if 语句一样,switch 语句也有表达式形式:

let anotherCharacter: Character = "a"
let message = switch anotherCharacter {
case "a":
    "The first letter of the Latin alphabet"
case "z":
    "The last letter of the Latin alphabet"
default:
    "Some other character"
}

print(message)
// Prints "The first letter of the Latin alphabet".

在这个示例中,switch 表达式中的每个 case 都包含当该 case 匹配 anotherCharacter 时要用于 message 的值。因为 switch 总是穷尽的,所以总有一个值可以赋值。

if 表达式一样,你可以抛出错误或调用永不返回的函数(如 fatalError(_:file:line:))来代替为给定 case 提供值。你可以在赋值的右侧使用 switch 表达式(如上面的示例所示),也可以将它们用作函数或闭包返回的值。

不存在隐式贯穿

与 C 和 Objective-C 中的 switch 语句不同,Swift 中的 switch 语句默认不会从每个 case 的底部贯穿到下一个 case。相反,整个 switch 语句在第一个匹配的 switch case 完成后立即结束执行,无需显式 break 语句。这使得 switch 语句比 C 中的更安全、更易于使用,并且避免了错误地执行多个 switch case。

注意 尽管 Swift 中不需要 break,但你可以使用 break 语句来匹配并忽略特定 case,或在该 case 完成执行之前跳出已匹配的 case。有关详细信息,请参阅在 Switch 语句中使用 Break

每个 case 的主体必须包含至少一条可执行语句。编写以下代码是无效的,因为第一个 case 是空的:

let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // Invalid, the case has an empty body
case "A":
    print("The letter A")
default:
    print("Not the letter A")
}
// This will report a compile-time error.

与 C 中的 switch 语句不同,这个 switch 语句不会同时匹配 "a""A"。相反,它会报告编译时错误,指出 case "a": 不包含任何可执行语句。这种方法避免了从一个 case 意外贯穿到另一个 case,使代码更安全、意图更清晰。

要创建一个同时匹配 "a""A"switch,将两个值组合成一个复合 case,用逗号分隔这些值。

let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
    print("The letter A")
default:
    print("Not the letter A")
}
// Prints "The letter A".

为了便于阅读,复合 case 也可以写成多行。有关复合 case 的更多信息,请参阅复合 Case

注意 要在特定 switch case 的末尾显式贯穿,请使用 fallthrough 关键字,如Fallthrough 中所述。

区间匹配

switch case 中的值可以检查是否包含在某个区间内。此示例使用数字区间为任意大小的数字提供自然语言计数:

let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String
switch approximateCount {
case 0:
    naturalCount = "no"
case 1..<5:
    naturalCount = "a few"
case 5..<12:
    naturalCount = "several"
case 12..<100:
    naturalCount = "dozens of"
case 100..<1000:
    naturalCount = "hundreds of"
default:
    naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")
// Prints "There are dozens of moons orbiting Saturn."

在上面的示例中,approximateCountswitch 语句中被评估。每个 case 将该值与数字或区间进行比较。因为 approximateCount 的值落在 12 和 100 之间,naturalCount 被赋值为 "dozens of",然后执行转移出 switch 语句。

元组

你可以使用元组在同一个 switch 语句中测试多个值。元组的每个元素可以针对不同的值或值区间进行测试。或者,使用下划线字符(_),也称为通配符模式,来匹配任何可能的值。

下面的示例取一个 (x, y) 点,表示为类型 (Int, Int) 的简单元组,并将其在随后的图表上分类。

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    print("\(somePoint) is at the origin")
case (_, 0):
    print("\(somePoint) is on the x-axis")
case (0, _):
    print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
    print("\(somePoint) is inside the box")
default:
    print("\(somePoint) is outside of the box")
}
// Prints "(1, 1) is inside the box".

coordinateGraphSimple

switch 语句确定该点是在原点 (0, 0)、在红色 x 轴上、在绿色 y 轴上、在以原点为中心的蓝色 4×4 方框内,还是在方框外。

与 C 不同,Swift 允许多个 switch case 考虑相同的值。实际上,点 (0, 0) 可以匹配此示例中所有四个 case。但是,如果可能有多个匹配,则始终使用第一个匹配的 case。点 (0, 0) 会首先匹配 case (0, 0),因此所有其他匹配的 case 将被忽略。

值绑定

switch case 可以将其匹配的一个或多个值命名为临时常量或变量,以便在 case 主体中使用。这种行为称为值绑定,因为值在 case 主体内绑定到临时常量或变量。

下面的示例取一个 (x, y) 点,表示为类型 (Int, Int) 的元组,并将其在随后的图表上分类:

let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
    print("on the x-axis with an x value of \(x)")
case (0, let y):
    print("on the y-axis with a y value of \(y)")
case let (x, y):
    print("somewhere else at (\(x), \(y))")
}
// Prints "on the x-axis with an x value of 2".

coordinateGraphMedium

switch 语句确定该点是在红色 x 轴上、在绿色 y 轴上,还是在其他地方(不在任何轴上)。

三个 switch case 声明占位符常量 xy,它们临时从 anotherPoint 获取一个或两个元组值。第一个 case,case (let x, 0),匹配任何 y 值为 0 的点,并将该点的 x 值赋给临时常量 x。类似地,第二个 case,case (0, let y),匹配任何 x 值为 0 的点,并将该点的 y 值赋给临时常量 y

声明临时常量后,它们可以在 case 的代码块中使用。这里,它们用于打印该点的分类。

这个 switch 语句没有 default case。最后一个 case,case let (x, y),声明了一个可以匹配任何值的两个占位符常量的元组。因为 anotherPoint 始终是两个值的元组,所以这个 case 匹配所有可能的剩余值,不需要 default case 来使 switch 语句穷尽。

Where

switch case 可以使用 where 子句来检查附加条件。

下面的示例在以下图表上分类一个 (x, y) 点:

let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
    print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
    print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
    print("(\(x), \(y)) is just some arbitrary point")
}
// Prints "(1, -1) is on the line x == -y".

coordinateGraphComplex

switch 语句确定该点是在绿色对角线 x == y 上、在紫色对角线 x == -y 上,还是都不在。

三个 switch case 声明占位符常量 xy,它们临时从 yetAnotherPoint 获取两个元组值。这些常量用作 where 子句的一部分,以创建动态过滤器。只有当 where 子句的条件对该值评估为 true 时,switch case 才匹配当前的 point 值。

与前一个示例一样,最后一个 case 匹配所有可能的剩余值,因此不需要 default case 来使 switch 语句穷尽。

复合 Case

共享同一主体的多个 switch case 可以通过在 case 后写多个模式来组合,每个模式之间用逗号分隔。如果任何模式匹配,则认为该 case 匹配。如果列表很长,模式可以写成多行。例如:

let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
    print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
    "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
    print("\(someCharacter) is a consonant")
default:
    print("\(someCharacter) isn't a vowel or a consonant")
}
// Prints "e is a vowel".

switch 语句的第一个 case 匹配英语中所有五个小写元音字母。类似地,第二个 case 匹配所有小写英语辅音字母。最后,default case 匹配任何其他字符。

复合 case 也可以包含值绑定。复合 case 的所有模式都必须包含相同的值绑定集,并且每个绑定都必须从复合 case 中的所有模式获取相同类型的值。这确保了无论复合 case 的哪个部分匹配,case 主体中的代码始终可以访问绑定的值,并且该值始终具有相同的类型。

let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
    print("On an axis, \(distance) from the origin")
default:
    print("Not on an axis")
}
// Prints "On an axis, 9 from the origin".

上面的 case 有两个模式:(let distance, 0) 匹配 x 轴上的点,(0, let distance) 匹配 y 轴上的点。两个模式都包含 distance 的绑定,并且 distance 在两个模式中都是整数——这意味着 case 主体中的代码始终可以访问 distance 的值。

模式

在前面的示例中,每个 switch case 都包含一个模式,指示哪些值与该 case 匹配。你也可以使用模式作为 if 语句的条件。以下是它的样子:

let somePoint = (12, 100)
if case (let x, 100) = somePoint {
    print("Found a point on the y=100 line, at \(x)")
}
// Prints "Found a point on the y=100 line, at 12".

在这段代码中,if 语句的条件以 case 开头,表示该条件是模式而不是布尔值。如果模式匹配,则 if 的条件被认为是真的,因此 if 语句主体中的代码将运行。你可以在 if case 后写的模式与 switch case 中可以写的模式相同。

for-in 循环中,你可以使用值绑定模式为值的各个部分命名,即使在代码中不写 case

let points = [(10, 0), (30, -30), (-20, 0)]

for (x, y) in points {
    if y == 0 {
        print("Found a point on the x-axis at \(x)")
    }
}
// Prints "Found a point on the x-axis at 10".
// Prints "Found a point on the x-axis at -20".

上面的 for-in 循环遍历元组数组,将元组的第一个和第二个元素绑定到 xy 常量。循环内的语句可以使用这些常量,例如检查点是否在 x 轴上的 if 语句。编写此代码的更简洁方式是使用 for-case-in 循环组合值绑定和条件。下面的代码与上面的 for-in 循环行为相同:

for case (let x, 0) in points {
    print("Found a point on the x-axis at \(x)")
}
// Prints "Found a point on the x-axis at 10".
// Prints "Found a point on the x-axis at -20".

在这段代码中,条件作为模式的一部分集成到 for-case-in 循环中。for-case-in 循环中的语句仅对 x 轴上的点运行。此代码产生与上面 for-in 循环相同的结果,但是是一种更紧凑的方式来仅遍历集合中的某些元素。

for-case-in 循环也可以包含 where 子句,以检查附加条件。循环内的语句仅在 where 子句与当前元素匹配时运行。例如:

for case let (x, y) in points where x == y || x == -y  {
    print("Found (\(x), \(y)) along a line through the origin")
}
// Prints "Found (30, -30) along a line through the origin".

此代码将元组的第一个和第二个元素作为常量绑定到 xy,然后在 where 子句中检查它们的值。如果 where 子句为 true,则 for 循环主体中的代码运行;否则,继续迭代下一个元素。

因为模式可以绑定值,所以 if-case 语句和 for-case-in 循环对于处理具有关联值的枚举很有用,如关联值中所述。

控制转移语句

控制转移语句通过将控制从一段代码转移到另一段代码来改变代码的执行顺序。Swift 有五个控制转移语句:

  • continue
  • break
  • fallthrough
  • return
  • throw

continuebreakfallthrough 语句将在下面描述。return 语句在函数中描述,throw 语句在使用抛出函数传播错误中描述。

Continue

continue 语句告诉循环停止正在做的事情,并在循环的下一次迭代开始时重新开始。它表示”我已完成当前循环迭代”,但不完全离开循环。

以下示例从小写字符串中删除所有元音和空格,以创建一个神秘的谜语短语:

let puzzleInput = "great minds think alike"
var puzzleOutput = ""
let charactersToRemove: [Character] = ["a", "e", "i", "o", "u", " "]
for character in puzzleInput {
    if charactersToRemove.contains(character) {
        continue
    }
    puzzleOutput.append(character)
}
print(puzzleOutput)
// Prints "grtmndsthnklk".

上面的代码在每次匹配元音或空格时调用 continue 关键字,导致循环的当前迭代立即结束并直接跳转到下一次迭代的开始。

Break

break 语句立即结束整个控制流语句的执行。当你想比正常情况更早终止 switch 或循环语句的执行时,可以在 switch 或循环语句内使用 break 语句。

循环语句中的 Break

在循环语句中使用时,break 立即结束循环的执行,并将控制转移到循环结束大括号(})之后的代码。当前循环迭代中不再执行任何代码,也不会开始循环的进一步迭代。

Switch 语句中的 Break

switch 语句中使用时,break 使 switch 语句立即结束其执行,并将控制转移到 switch 语句结束大括号(})之后的代码。

这种行为可用于匹配并忽略 switch 语句中的一个或多个 case。因为 Swift 的 switch 语句是穷尽的且不允许空 case,所以有时需要故意匹配并忽略一个 case 以使你的意图明确。你可以通过将 break 语句作为要忽略的 case 的整个主体来做到这一点。当该 case 被 switch 语句匹配时,case 内的 break 语句立即结束 switch 语句的执行。

注意 仅包含注释的 switch case 会被报告为编译时错误。注释不是语句,不会导致 switch case 被忽略。始终使用 break 语句来忽略 switch case。

以下示例对一个 Character 值进行 switch,并确定它是否代表四种语言之一中的数字符号。为简洁起见,多个值包含在单个 switch case 中。

let numberSymbol: Character = "三"  // Chinese symbol for the number 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
    possibleIntegerValue = 1
case "2", "٢", "二", "๒":
    possibleIntegerValue = 2
case "3", "٣", "三", "๓":
    possibleIntegerValue = 3
case "4", "٤", "四", "๔":
    possibleIntegerValue = 4
default:
    break
}
if let integerValue = possibleIntegerValue {
    print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
    print("An integer value couldn't be found for \(numberSymbol).")
}
// Prints "The integer value of 三 is 3."

此示例检查 numberSymbol 以确定它是否是拉丁文、阿拉伯文、中文或泰文的数字 14 的符号。如果找到匹配,switch 语句的其中一个 case 将一个可选 Int? 变量 possibleIntegerValue 设置为适当的整数值。

switch 语句完成执行后,示例使用可选绑定来确定是否找到了值。由于 possibleIntegerValue 是可选类型,它具有隐式初始值 nil,因此只有当 possibleIntegerValueswitch 语句的前四个 case 之一设置为实际值时,可选绑定才会成功。

因为在上面的示例中列出每个可能的 Character 值是不切实际的,default case 处理任何未匹配的字符。这个 default case 不需要执行任何操作,因此它只以一条 break 语句作为其主体。一旦 default case 匹配,break 语句结束 switch 语句的执行,代码从 if let 语句继续执行。

Fallthrough

在 Swift 中,switch 语句不会从每个 case 的底部贯穿到下一个 case。也就是说,整个 switch 语句在第一个匹配的 case 完成后立即完成执行。相比之下,C 要求你在每个 switch case 的末尾插入显式 break 语句以防止贯穿。避免默认贯穿意味着 Swift 的 switch 语句比 C 中的对应语句更简洁和可预测,因此它们避免了错误地执行多个 switch case。

如果你需要 C 风格的贯穿行为,可以使用 fallthrough 关键字逐个 case 选择启用此行为。下面的示例使用 fallthrough 创建数字的文本描述。

let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " a prime number, and also"
    fallthrough
default:
    description += " an integer."
}
print(description)
// Prints "The number 5 is a prime number, and also an integer."

此示例声明一个名为 description 的新 String 变量并为其分配初始值。然后函数使用 switch 语句考虑 integerToDescribe 的值。如果 integerToDescribe 的值是列表中的质数之一,函数将文本追加到 description 的末尾,以指出该数字是质数。然后它使用 fallthrough 关键字”贯穿”到 default case。default case 在描述末尾添加一些额外文本,然后 switch 语句完成。

除非 integerToDescribe 的值在已知质数列表中,否则它根本不会被第一个 switch case 匹配。因为没有其他特定 case,integerToDescribedefault case 匹配。

switch 语句完成执行后,使用 print(_:separator:terminator:) 函数打印数字的描述。在此示例中,数字 5 被正确识别为质数。

注意 fallthrough 关键字不会检查它导致执行进入的 switch case 的条件。fallthrough 关键字只是使代码执行直接移动到下一个 case(或 default case)块内的语句,就像 C 的标准 switch 语句行为一样。

带标签的语句

在 Swift 中,你可以在其他循环和条件语句内嵌套循环和条件语句,以创建复杂的控制流结构。但是,循环和条件语句都可以使用 break 语句提前结束其执行。因此,有时明确指出你希望 break 语句终止哪个循环或条件语句是有用的。类似地,如果你有多个嵌套循环,明确指出 continue 语句应该影响哪个循环可能很有用。

为了实现这些目的,你可以用语句标签标记循环语句或条件语句。对于条件语句,你可以将语句标签与 break 语句一起使用来结束带标签语句的执行。对于循环语句,你可以将语句标签与 breakcontinue 语句一起使用来结束或继续带标签语句的执行。

带标签的语句通过将标签放在语句的引入关键字同一行上来指示,后跟冒号。以下是 while 循环的这种语法示例,尽管该原则对所有循环和 switch 语句都相同:

<#label name#>: while <#condition#> {
   <#statements#>
}

以下示例使用带标签的 while 循环的 breakcontinue 语句,用于你在本章前面看到的蛇梯棋游戏的改编版本。这次,游戏有一条额外规则:

  • 要获胜,你必须恰好落在第 25 格。

如果特定骰子点数会让你超过第 25 格,你必须重新掷骰子,直到掷出恰好能让你落在第 25 格的数字。

游戏棋盘与之前相同。

snakesAndLadders

finalSquareboardsquarediceRoll 的值初始化方式与之前相同:

let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0

这个版本的游戏使用 while 循环和 switch 语句来实现游戏逻辑。while 循环有一个名为 gameLoop 的语句标签,表示它是蛇梯棋游戏的主游戏循环。

while 循环的条件是 while square != finalSquare,以反映你必须恰好落在第 25 格。

gameLoop: while square != finalSquare {
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    switch square + diceRoll {
    case finalSquare:
        // diceRoll will move us to the final square, so the game is over
        break gameLoop
    case let newSquare where newSquare > finalSquare:
        // diceRoll will move us beyond the final square, so roll again
        continue gameLoop
    default:
        // this is a valid move, so find out its effect
        square += diceRoll
        square += board[square]
    }
}
print("Game over!")

每次循环开始时掷骰子。循环不是立即移动玩家,而是使用 switch 语句来考虑移动的结果并确定是否允许移动:

  • 如果骰子点数将玩家移动到最终方格,游戏结束。break gameLoop 语句将控制转移到 while 循环之外的第一行代码,结束游戏。
  • 如果骰子点数将玩家移动到超过最终方格,该移动无效,玩家需要再次掷骰子。continue gameLoop 语句结束当前 while 循环迭代,并开始循环的下一次迭代。
  • 在所有其他情况下,骰子点数是有效的移动。玩家向前移动 diceRoll 格,游戏逻辑检查是否有蛇或梯子。然后循环结束,控制返回到 while 条件以决定是否需要另一个回合。

注意 如果上面的 break 语句没有使用 gameLoop 标签,它将跳出 switch 语句,而不是 while 语句。使用 gameLoop 标签清楚地表明应该终止哪个控制语句。

当调用 continue gameLoop 跳转到循环的下一次迭代时,并非严格必须使用 gameLoop 标签。游戏中只有一个循环,因此关于 continue 语句将影响哪个循环没有歧义。但是,使用 gameLoop 标签与 continue 语句无害。这样做与标签在 break 语句旁边的使用保持一致,并有助于使游戏逻辑更清晰、更易于阅读和理解。

提前退出

guard 语句与 if 语句一样,根据表达式的布尔值执行语句。你使用 guard 语句要求条件必须为真,guard 语句之后的代码才能执行。与 if 语句不同,guard 语句总是有 else 子句——如果条件不为真,则执行 else 子句内的代码。

func greet(person: [String: String]) {
    guard let name = person["name"] else {
        return
    }

    print("Hello \(name)!")

    guard let location = person["location"] else {
        print("I hope the weather is nice near you.")
        return
    }

    print("I hope the weather is nice in \(location).")
}

greet(person: ["name": "John"])
// Prints "Hello John!"
// Prints "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// Prints "Hello Jane!"
// Prints "I hope the weather is nice in Cupertino."

如果 guard 语句的条件满足,代码执行在 guard 语句的结束大括号之后继续。作为条件一部分使用可选绑定赋值的任何变量或常量在 guard 语句所在的其余代码块中可用。

如果该条件不满足,则执行 else 分支内的代码。该分支必须转移控制以退出 guard 语句所在的代码块。它可以使用控制转移语句(如 returnbreakcontinuethrow)来做到这一点,或者它可以调用不返回的函数或方法,如 fatalError(_:file:line:)

使用 guard 语句进行要求检查与使用 if 语句进行相同检查相比,提高了代码的可读性。它让你编写通常执行的代码而无需将其包装在 else 块中,并让你将处理违反要求的代码放在要求旁边。

延迟操作

ifwhile 等控制流结构(让你控制是否执行部分代码或执行多少次)不同,defer 控制何时执行一段代码。你使用 defer 块来编写将在程序到达当前作用域末尾时稍后执行的代码。例如:

var score = 1
if score < 10 {
    defer {
        print(score)
    }
    score += 5
}
// Prints "6".

在上面的示例中,defer 块内的代码在退出 if 语句主体之前执行。首先,if 语句中的代码运行,将 score 增加 5。然后,在退出 if 语句的作用域之前,延迟的代码运行,打印 score

defer 内的代码总是运行,无论程序如何退出该作用域。这包括提前从函数返回、跳出 for 循环或抛出错误等代码。这种行为使 defer 对于需要保证一对操作发生的操作很有用——比如手动分配和释放内存、打开和关闭低级文件描述符、以及开始和结束数据库中的事务——因为你可以在代码中将两个操作写在一起。例如,以下代码通过在一段代码内加减 100 来给分数一个临时奖励:

var score = 3
if score < 100 {
    score += 100
    defer {
        score -= 100
    }
    // Other code that uses the score with its bonus goes here.
    print(score)
}
// Prints "103".

如果你在同一作用域中写了多个 defer 块,你指定的第一个是最后一个运行的。

if score < 10 {
    defer {
        print(score)
    }
    defer {
        print("The score is:")
    }
    score += 5
}
// Prints "The score is:".
// Prints "6".

如果你的程序停止运行——例如,因为运行时错误或崩溃——延迟的代码不会执行。但是,延迟的代码确实会在抛出错误后执行;有关将 defer 与错误处理一起使用的信息,请参阅指定清理操作

检查 API 可用性

Swift 内置支持检查 API 可用性,这确保你不会意外使用在给定部署目标上不可用的 API。

编译器使用 SDK 中的可用性信息来验证代码中使用的所有 API 在项目指定的部署目标上都可用。如果你尝试使用不可用的 API,Swift 会在编译时报告错误。

你在 ifguard 语句中使用可用性条件来根据你要使用的 API 在运行时是否可用来有条件地执行代码块。编译器在验证该代码块中的 API 是否可用时使用来自可用性条件的信息。

if #available(iOS 10, macOS 10.12, *) {
    // Use iOS 10 APIs on iOS, and use macOS 10.12 APIs on macOS
} else {
    // Fall back to earlier iOS and macOS APIs
}

上面的可用性条件指定在 iOS 中,if 语句的主体仅在 iOS 10 及更高版本中执行;在 macOS 中,仅在 macOS 10.12 及更高版本中执行。最后一个参数 * 是必需的,它指定在任何其他平台上,if 的主体在你的目标指定的最小部署目标上执行。

在其一般形式中,可用性条件采用平台名称和版本的列表。你使用平台名称如 iOSmacOSwatchOStvOSvisionOS——有关完整列表,请参阅声明属性。除了指定主要版本号如 iOS 8 或 macOS 10.10 外,你还可以指定次要版本号如 iOS 11.2.6 和 macOS 10.13.3。

if #available(<#platform name#> <#version#>, <#...#>, *) {
    <#statements to execute if the APIs are available#>
} else {
    <#fallback statements to execute if the APIs are unavailable#>
}

当你将可用性条件与 guard 语句一起使用时,它会细化用于该代码块中其余代码的可用性信息。

@available(macOS 10.12, *)
struct ColorPreference {
    var bestColor = "blue"
}

func chooseBestColor() -> String {
    guard #available(macOS 10.12, *) else {
       return "gray"
    }
    let colors = ColorPreference()
    return colors.bestColor
}

在上面的示例中,ColorPreference 结构需要 macOS 10.12 或更高版本。chooseBestColor() 函数以可用性 guard 开始。如果平台版本太旧而无法使用 ColorPreference,它会回退到始终可用的行为。在 guard 语句之后,你可以使用需要 macOS 10.12 或更高版本的 API。

除了 #available,Swift 还支持使用不可用性条件进行相反的检查。例如,以下两个检查做同样的事情:

if #available(iOS 10, *) {
} else {
    // Fallback code
}

if #unavailable(iOS 10) {
    // Fallback code
}

当检查只包含回退代码时,使用 #unavailable 形式有助于使代码更具可读性。