Swift4.2 控制流

Swift 提供了多种控制流结构。其中包含 while 循环来执行多次任务; ifguardswitch 语句来执行特定条件下不同的代码分支; 还有 breakcontinue 语句使控制流跳转到你代码中的其他位置。

Swift 还提供了 for-in 循环用来更简便的遍历数组(arrays), 字典(dictionaries),区间(ranges),字符串(strings),和其他序列类型。

Swift 的 switch 语句比其他的类 C 语言更加强大。case 可以匹配多种不同的模式,包括间隔匹配(interval matches),元祖(tuples),和转换到特定类型。switch 语句的 case 体中匹配的值可以绑定临时常量或变量,在每个 case 中也可以使用 where 来实现更复杂的匹配情况。

For-In 循环

可以使用 for-in 循环来遍历序列中的所有元素,例如数组中的所有元素,数字的范围,或者字符串的字符。

你也可以通过遍历一个字典来访问它的键值对。遍历字典时其中的每个元素都会返回成 (key, value) 元组(Tuple)的形式, 你也可以在 for-in 循环中显式的命名常量来分解 (key, value)元组。 在下面的例子中,字典中的值(Key)被分解为 animalName 常量,字典中的值(Value)被分解为 legCount 常量。

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

上面的例子中,常数 index 的值在每次循环开始时都会自动赋值。因此,index 不需要在使用前进行声明。只要声明循环时,包含了该常量,就会对其进行隐式声明,不需要使用声明关键词 let

如果你不需要使用区间中的所有值,你可以使用 - 替代变量名来忽略对应的值。

1
2
3
4
5
6
7
8
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"

在一些情况中你可能不想使用包含两个端点的闭区间。想象在手表表面上画每分钟的刻度标记。你想要从 0 分钟开始画 60 个刻度标记。可以使用半开区间操作符(..<)来包含下界但不包含上界。

1
2
3
4
let minutes = 60
for tickMark in 0..<minutes {
// 每分钟渲染一个刻度线(60 次)
}

一些用户在他们的界面上可能想要更少的刻度标记。他们可能更喜欢每 5 分钟一个刻度。使用 stride(from:to:by:) 函数可以跳过不需要的标记。

1
2
3
4
let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
// 每 5 分钟打一个标记(0, 5, 10, 15 ... 45, 50, 55)
}

通过 stride(from:through:by:) 使用闭区间也是可以的:

1
2
3
4
5
let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
// 每 3 小时打一个标记(3, 6, 9, 12)
}

While 循环

一个 while 循环会一直执行一组语句直到条件变为 false 。这类循环最适合第一次循环前不知道循环的次数的情况。Swift 提供两种类型的 while 循环:

  • while 在每次循环开始时判断条件。
  • repeat-while 在每次循环结束时判断条件。

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

1
2
3
repeat {
statements
} while condition

条件语句

Swift 提供两种条件语句:if 语句和 switch 语句。通常,使用 if 语句来执行结果可能性较少的简单条件;switch 语句则更适合于有较多组合的更复杂的条件,而且,当需要使用模式匹配来判断执行合适的代码段时,switch 语句会更有用。

If

if 语句最简单的形式只有一个 if 条件,而且只有当这个条件为 true 时才会执行对应的代码。

Switch

switch 语句会将某一个值与其它几种可能匹配的模式进行比较,然后它会执行第一个匹配成功的模式下对应的代码块。当可能的情形非常多时,应该使用 switch 语句替代 if 语句。

switch 语句最简单的形式是将一个值和另外一个或几个同类型的值进行比较。

1
2
3
4
5
6
7
8
9
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 语句必须是 可穷尽的。 也就是说,判断的类型的每个可能的值都要有一个 switch 的分支(case)与之对应。为每个可能的值创建一个分支是不合理的,你可以定义一个默认分支来覆盖没有单独处理的其他所有值。这个默认分支使用 default 关键字声明,并且必须放在最后。

下面例子使用 switch 语句匹配名为 someCharacter 的单个小写字符:

1
2
3
4
5
6
7
8
9
10
let someCharacter: Character = "z"
switch someCharacter {
case "a":
print("The first letter of the alphabet")
case "z":
print("The last letter of the alphabet")
default:
print("Some other character")
}
// 打印 "The last letter of the alphabet"

不存在隐式的贯穿

与 C 语言和 Objective-C 中的 switch 语句相反,Swift 中的 switch 语句在执行完一个分支后不会「贯穿」到下一个分支。相反,整个 switch 语句一旦完成第一个匹配的 switch 分支就会结束,而不需要明确的 break 语句。这使得 Swift 中的 switch 语句比 C 语言中的更加安全、易用,并且避免了错误地执行多个 switch 分支的情况。

注意
虽然在 Swift 中 break 不是必须的,你可以使用 break 语句来匹配和忽略特定的分支或者或者在分支全部执行前跳出。更多细节,查看 Switch 语句中的 Break

区间匹配

switch 中分支匹配的值也可以是一个区间。这个例子使用数字区间来匹配任意数字对应的自然语言格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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."

元组

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

下面的示例声明了一个 (x, y) 点,该变量是类型为 (Int, Int) 的元组,并将其显示在示例后面的图上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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")
}
// 打印 "(1, 1) is inside the box"

与 C 语言不同,Swift 允许同一个值符合多个 switch 分支。实际上,在这个例子中,点 (0, 0) 匹配所有四个分支。但是,如果匹配多个分支,则始终使用第一个匹配的分支。点 (0, 0) 首先匹配 case (0, 0),因此所有其他的匹配分支都被忽略。

值绑定

switch 分支可以将其匹配的一个值或多个值赋值给临时的常量或变量,常量或变量可以在 case 主体中使用。这个行为被称为值绑定,因为值在 case 主体中被绑定给临时的常量或变量。

下面的示例声明了一个 (x, y) 点,其类型为 (Int, Int) 的元组,并且该点展示在示例后面的图上:

1
2
3
4
5
6
7
8
9
10
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))")
}
// 打印 "on the x-axis with an x value of 2"

三个 switch 分支声明了占位符常量 xy,暂时从 anotherPoint 中获取一个或多个元组值。第一个分支 case (let x, 0) 匹配任何 y 值为 0 的点,并把 x 的值赋值给临时常量 x。同样地,第二个分支 case (0, let y) 匹配任何 x 值为 0 的点,并把 y 的值赋值给临时常量 y

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

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

Where

switch 分支中可以使用 where 子句来检测额外的条件。

1
2
3
4
5
6
7
8
9
10
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")
}
// 打印 "(1, -1) is on the line x == -y"

三个 switch 分支声明了占位符常量 xy,它们从 yetAnotherPoint 中获取两个元组值。这些常量用作 where 子句的一部分,用来创建一个动态分类器。只有当 where 子句满足计算值为 true 时,switch 分支才匹配当前的 point

与前一个示例一样,最后一个 case 匹配所有剩余的值,所以不需要 default 分支来使 switch 语句穷举。

复合分支

case 后面写多个模式可以把多个分支共享在同一个主体中,每个模式用逗号隔开。如果任何一个模式匹配,那么这个分支就是匹配的。如果模式太多,可以把模式写为多行。比如:

1
2
3
4
5
6
7
8
9
10
11
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) is not a vowel or a consonant")
}
// 打印 "e is a vowel"

switch 语句的第一个分支匹配英语中的所有五个小写元音。同样,第二个分支匹配所有的小写辅音。最后,default 分支匹配其余字符。

复合分支也可以包含值绑定。复合分支的所有模式必须包含在同一组值绑定中,并且每个绑定必须从复合分支的所有模式中获取相同类型的值。这样确保无论复合分支中哪个部分匹配,分支主体的代码总是可以访问绑定的值,并且确保值总是有相同的类型。

1
2
3
4
5
6
7
8
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")
}
// 打印 "On an axis, 9 from the origin"

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

控制转移语句

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

  • continue
  • break
  • fallthrough
  • return
  • throw

Continue

continue 语句告诉循环停止正在做的事情,并在循环的下一次迭代开始时再启动。它仿佛在说「我完成了当前的循环迭代」而没有完全离开循环。

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

1
2
3
4
5
6
7
8
9
10
11
12
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
} else {
puzzleOutput.append(character)
}
}
print(puzzleOutput)
// 打印 "grtmndsthnklk"

上面的代码只要匹配到元音或空格时就调用 continue 关键字,使循环的本次迭代立即结束并跳到下一次迭代的开始。

Break

break 语句立即结束整个控制流语句的执行,当你想在 switch 或循环中提前结束时,可以在 switch 或循环中使用 break 语句。

在循环语句中使用 Break

在循环语句中使用时,break 立即结束循环的执行,并把控制转移到循环右括号(})后面的代码上。不执行来自当前循环迭代的下一步代码,并且不再开始循环的迭代。

在 Switch 语句中使用 Break

switch 语句中使用时,break 会使 switch 语句立即结束执行,并把控制转移到 switch 语句的右括号(})后面的代码上。

此行为可用于匹配和忽略 switch 语句中的一个或多个分支。 因为 Swiftswitch 语句是穷举的并且不允许空分支,所以有时需要故意匹配并忽略一个分支使你的意图明确。 你可以将 break 语句作为要忽略的分支的整个主体来使用。当该分支与 switch 语句匹配时,分支中的 break 语句使 switch 语句立即结束执行。

注意
如果 switch 分支只包含注释会报编译时错误。 注释不是语句,不会使 switch 分支被忽略。总是使用 break 语句来忽略 switch 分支。

贯穿

在 Swift 中, switch 语句的每个分支在判断结束后不会「贯穿」到下一个分支。即,整个 switch 语句会在第一个匹配的分支语句执行完成后终止。相反地,C 语言明确要求在每个 switch 分支结束时手动添加 break 语句来防止贯穿。相对而言,默认没有贯穿使得 Swift 中的 switch 语句更加简洁,可读性更强,并可以因此避免错误地执行多个 switch 分支。

如果需要像 C 语言中那样的贯穿行为,你可以在分支中逐个添加 fallthrough 关键字。 下面这个例子就利用了贯穿 fallthrough 来给数字添加描述。

1
2
3
4
5
6
7
8
9
10
11
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)
// 打印 "The number 5 is a prime number, and also an integer."

注意
fallthrough 关键字不会检查 switch 语句中下一个分支的条件,它只是让代码在执行的过程中直接进入下一个分支 (或 default 分支) 中的语句, 就像 C 语言中 switch 语句的标准行为。

带标签语句

在 Swift 中,你可以在循环体和条件语句中嵌套循环体和条件语句来创造复杂的控制流结构。并且,循环体和条件语句都可以使用 break 语句来提前结束整个代码块。因此,显式地指明 break 语句想要终止的是哪个循环体或者条件语句,会很有用。类似地,如果你有许多嵌套的循环体,显式指明 continue 语句想要影响哪一个循环体也会非常有用。

为了实现这个目的,你可以使用标签( statement label )来标记一个循环体或者条件语句,对于一个条件语句,你可以使用 break 加标签的方式,来结束这个被标记的语句。对于一个循环语句,你可以使用 break 或者 continue 加标签,来结束或者继续这条被标记语句的执行。

声明一个带标签的语句是通过在该语句的关键词的同一行前面放置一个标签,作为这个语句的前导关键字( introducor keyword ),并且该标签后面跟随一个冒号。下面是一个针对 while 循环体的标签语法,同样的规则适用于所有的循环体和条件语句。

1
2
3
label name: while condition {
statements
}

提前退出

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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"])
// 打印 "Hello John!"
// 打印 "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// 打印 "Hello Jane!"
// 打印 "I hope the weather is nice in Cupertino."

如果满足 guard 语句的条件,则在 guard 声明的结束括号后继续执行代码。 当任何变量或常量在使用可选绑定作为条件被赋值后,它的值都可用于 guard 语句后的其余代码块。

如果不满足该条件,则执行 else 分支内的代码。 该分支必须转移控制以退出 guard 语句后的代码块。 它可以通过控制转移语句来执行此操作,例如 returnbreakcontinuethrow ,也可以调用一个无返回值的函数或方法,例如 fatalError(_:file:line:)

相比于使用 if 语句进行判断,使用 guard 语句可以提高代码的可读性。 它可以让你编写出连贯执行的代码,而不必将其包装在 else 块中,并且让你更加从容地处理异常代码。

检测 API 可用性

Swift 内置支持检查 API 可用性,这可以确保我们不会在当前部署机器上,不小心地使用了不可用的 API。

编译器使用 SDK 中的可用信息来验证我们的代码中使用的所有 API 在项目指定的部署目标上是否可用。如果我们尝试使用一个不可用的 API,Swift 会在编译时报错。

我们在 ifguard 语句中使用 可用性条件(availability condition)去有条件的执行一段代码,来在运行时判断调用的 API 是否可用。编译器使用从可用性条件语句中获取的信息去验证,在这个代码块中调用的 API 是否可用。

1
2
3
4
5
if #available(iOS 10, macOS 10.12, *) {
// 在 iOS 使用 iOS 10 的 API, 在 macOS 使用 macOS 10.12 的 API
} else {
// 使用先前版本的 iOS 和 macOS 的 API
}

以上可用性条件指定, if 语句的代码块仅仅在 iOS 10 或 macOS 10.12 及更高版本才运行。最后一个参数,*,是必须的,用于指定在所有其它平台中,如果版本号高于你的设备指定的最低版本, if 语句的代码块将会运行。

在它一般的形式中,可用性条件使用了一个平台名字和版本的列表。平台名字可以是 iOS , macOS , watchOS , 和 tvOS —请访问声明属性来获取完整列表。 请参阅 Declaration Attributes。除了指定像 iOS 8 或 macOS 10.10 的大版本号,也可以指定像 iOS 11.2.6 以及 macOS 10.13.3 的小版本号。

1
2
3
4
5
if #available(platform name version, ..., *) {
APIs 可用,语句将执行
} else {
APIs 不可用,语句将不执行
}