Swift Learning (7) - Control Flow (Fully Ver.)

Swift Learning (7) - Control Flow (Fully Ver.)

August 21, 2021·Jensen
Jensen

image

Swift提供了多种流程控制结构,包括可以多次执行任务的while循环,基于特定条件选择执行不同代码分支的ifguardswitch语句,还有控制流程跳转到其他代码位置的breakcontinue语句。

Swift还提供了for-in循环,用来更简单地遍历数组(Array),字典(Dictionary),区间(Range),字符串(String)和其他序列类型。

Swiftswitch语句比许多类C语言要更加强大。case还可以匹配很多不同的模式,包括范围匹配,元组(tuple)和特定类型匹配。switch语句的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!
---
output: Hello, Anna!
Hello, Alex!
Hello, Brian!
Hello, Jack!

也可以通过遍历一个字典来访问它的键值对。遍历字典时,字典的每项元素会以(Key, Value)元组的形式返回,可以在for-in循环中使用显式的常量名称来解读该元组。下面的例子中,字典的键会被声明为animalName常量,字典的值会被声明为legCount常量:

let numberOfLegs = ["Spider": 8, "Ant": 6 , "Cat": 4]
for (animalName, legCount) in numberOfLegs {
    print("\(animalName) has \(legCount) legs!")
}  // Cat has 4 legs!  // Ant has 6 legs!  // Spider has 8 legs!
---
output: Ant has 6 legs!
Cat has 4 legs!
Spider has 8 legs!

字典的内用理论上是无序的,遍历元素时的顺序是无法确定的。将元素插入字典的顺序并不会决定它们被遍历的顺序。

for-in循环还可以使用数字范围。下面的例子用来输出乘法表的一部分内容。

for index in 1...5 {  // 默认循环中的index是常量,其值不可以在循环中被更改。若使用变量可以 for var 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
---
output: 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

例子中用来进行遍历的元素是使用闭区间操作符...表示从1到5的数字区间。index被赋值为闭区间中的第一个数字(1),然后循环中的语句被执行一次。在本例中 ,这个循环只包含一个语句,用来输出当前index值所对应的乘5乘法表的结果。该语句执行后,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).")  // 输出"3 to the power of 10 is 59049."
---
output: 3 to the power of 10 is 59049.

这个例子计算base这个数的power次幂(本例中,是3的10次幂),从1(3的0次幂)开始做3的乘法,进行10次,使用1到10的闭区间循环。这个计算并不需要知道每一次循环中计数器的具体的值,只需要执行了正确的循环次数即可。下划线符号_(替代循环中的变量)能够忽略当前值,并不提供循环遍历时对值的访问。

在某些情况下,可能不想使用包括两个端点的闭区间。想象一下,在一个手表上绘制分钟的刻度线。总共60个刻度,从0开始。可以使用半开区间运算符..<来表示一个左闭右开的区间。

let minutes = 60
for tickMark in 0..<minutes {
    // 每一分钟都渲染一个刻度线
}

一些用户可能在其UI中需要较少的刻度,他们可以使用每5分钟作为一个刻度,使用stride(from:to:by:)函数跳过不需要的标记。

let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
    // 每5分钟渲染一个刻度线(0,5,10,15...50,55)
}

可以在闭区间使用stride(from:through:by:)函数起到同样地作用:

let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
    // 每3小时渲染一个刻度线(3,6,9,12)
}

以上示例使用for-in循环来遍历范围、数组、字典和字符串。可以用它们来遍历任何集合,包括实现了Sequence协议的自定义类或者集合类型。

使用for-in循环遍历集合Set

let fruitSet: Set<String> = ["Apple", "Orange", "Peach"]
for fruit in fruitSet.sorted() {
    print("I love \(fruit).")
}
---
output: I love Apple.
I love Orange.
I love Peach.

While循环

while循环会一直运行一段语句直到条件变成false。这类循环适合使用在第一次迭代前,迭代次数未知的情况下。Swift提供两种while循环形式:

  • while循环,每次在循环开始时计算条件是否符合;
  • repeat-while循环,每次在循环结束时计算条件是否符合;

while

while循环从计算一个条件开始,如果条件为true,会重复运行一段语句,直到条件变为false

下面是while循环的一般格式:

/*
while condition {
    statements
}
*/

下面的例子来玩一个叫做蛇和梯子(也叫做滑道和梯子)的小游戏:

Snakes And Ladders

游戏规则如下:

  • 游戏盘面包括 25 个方格,游戏目标是达到或者超过第 25 个方格;
  • 每一轮,你通过掷一个六面体骰子来确定你移动方块的步数,移动的路线由上图中横向的虚线所示;
  • 如果在某轮结束,你移动到了梯子的底部,可以顺着梯子爬上去;
  • 如果在某轮结束,你移动到了蛇的头部,你会顺着蛇的身体滑下去。

游戏盘面可以用一个Int数组来表达,数组的长度由一个finalSquare常量存储,用来初始化数组和检测最终胜利的条件。游戏盘面由26个Int 0值初始化,而不是25个(由0到25,一共26个):

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来表示11和3之间的差值。为了对齐语句,这里使用了一元正运算符+i和一元负运算符-i,并且小于10的数字都是用0补齐,这些语法的技巧并不是必要的,只是为了让代码看起来更加整洁。

玩家由左下角空白处编号为0的方格开始游戏,玩家第一次掷骰子后才会进入到游戏盘面:

var square = 0
var diceRoll = 0
while square < finalSquare {
    // 掷骰子
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    // 根据点数移动
    square += diceRoll
    if square < board.count {
        // 顺着梯子爬上去或者顺着🐍滑下来
        square += board[square]
    }
    print(square, terminator: " ")
}
print("Game Over!")
---
output: 1 11 4 8 13 8 18 20 23 27 Game Over!

本例中使用了最简单的方法来模拟掷骰子。diceRoll的值并不是一个随机数,而是以0为初始值,之后每一次while循环,diceRoll的值增加1,然后检测是否超出了最大值。当 diceRoll的值等于7时,就超过了骰子的最大值,会被重置为1。所以diceRoll的取值顺序会一直是1,2,3,4,5,6,1,2等。

掷完骰子后,玩家向前移动diceRoll个方格,如果玩家移动超过了第25个方格,这个时候游戏将会结束。为了应对这种情况,代码会首先判断square的值是否小于boardcount属性,只有小于才会在board[square]上增加square来向前或向后移动。

注意

如果没有这个检测(square < board.count),board[square]可能会越界访问board数组导致运行时的错误。

当本轮while循环运行完毕,会再检测循环条件是否需要再运行一次循环。如果玩家移动到或者超过第25个方格,循环条件结果为false,此时游戏结束。while循环比较适合本例中的这种情况,因为在while循环开始时,我们并不知道游戏要跑多久,只有在达成指定条件时循环才会结束。

Repeat-While

while循环的另外一种形式是repeat-while,它和while的区别是在判断循环条件之前先执行一次循环的代码块,然后重复循环直到条件为false

注意

Swift语言的repeat-while循环和其他语言中的do-while循环是类似的。

下面是repeat-while循环的一般格式:

/*
 repeat {
     statements
 } while condition
*/

还是蛇和梯子的游戏,使用repeat-while循环来替代while循环,finalSquareboardsquarediceRoll的值初始化同while循环时一样:

let FinalSquare = 25
var boards = [Int](repeating: 0, count: FinalSquare + 1)
boards[03] = +08; boards[06] = +11; boards[09] = +09; boards[10] = +02
boards[14] = -10; boards[19] = -11; boards[22] = -02; boards[24] = -08
var Square = 0
var DiceRoll = 0

repeat-while的循环版本,循环中第一步就需要去检测是否在梯子或者蛇的方块上,没有梯子会让玩家直接上到第25个方格,所以玩家不会通过梯子直接赢得游戏,这样在循环开始时先检测是否踩在梯子或者蛇上是安全的。

游戏开始时,玩家在第 0 个方格上,board[0] 一直等于 0, 不会有什么影响:

repeat {
    // 顺着梯子爬上去或者顺着蛇滑下去
    Square += boards[Square]
    // 掷骰子
    DiceRoll += 1
    if DiceRoll == 7 { DiceRoll = 1 }
    // 根据点数移动
    Square += DiceRoll
    print(Square, terminator: " ")
} while Square < FinalSquare
print("Game Over!")
---
output: 1 3 14 8 13 19 9 20 23 27 Game Over!

检测完玩家是否踩在梯子或者蛇上之后,开始掷骰子,然后玩家向前移动diceRoll个方格,本轮循环结束。

循环条件(while Square < FinalSquare)和while方式相同,但是只会在循环结束后进行计算。在这个游戏中,repeat-while表现得比while循环更好。repeat-while方式会在条件判断Square没有超出后直接运行Square += boards[Square],这种方式比起前面while循环的版本,可以省去数组越界的检查。


条件语句

根据特定的条件执行的代码通常都是十分有用的。当错误发生时,开发者可能想运行额外的代码;或者,当值太大或太小时,向用户显示一条消息。要实现这些功能,就需要使用条件语句。

Swift提供两种类型的条件语句:if语句和switch语句。通常,当条件为简单且可能情况很少时,使用if语句。而switch语句更适用于当条件复杂、有更多排列组合的时候。并且switch在需要用到模式匹配(Pattern-Matching)的情况下会更有用。

If

if语句最简单的形式就是只包含一个条件,只有该条件为true时,才执行相关代码:

var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
    print("It's very cold! Consider wearing a scarf.")
}  // 输出"It's very cold! Consider wearing a scarf."
---
output: It's very cold! Consider wearing a scarf.

上面的例子会判断温度是否小于等于32华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行if代码块后面的代码。

当然,if语句允许二选一执行,叫做else从句。也就是当条件为false时,执行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.")
}  // 输出"It's not that cold. Wear a t-shirt."
---
output: 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.")
}  // 输出“It's really warm. Don't forget to wear sunscreen.”
---
output: 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分支,也就不会打印任何消息。

Switch

switch语句会尝试把某个值与若干个模式(pattern)进行匹配,根据第一个匹配成功的模式,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关键字开始,为了匹配某些更特定的值,Swift提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。

if语句类似,每一个case都是代码执行的一条分支,switch语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值切换(Switching)。

switch语句必须是完备的。这就是说,每一个可能的值都必须至少有一个case分支与之对应。在某些不可能涵盖所有值的情况下,可以使用默认(default)分支来涵盖其他所有没有对应的值,这个默认分支必须在switch语句的最后面

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

let someCharacter: Character = "z"
switch someCharacter {
case "a":
    print("The 1st letter of the alphabet.")
case "z":
    print("The last letter of the alphabet.")
default:
    print("Some other character.")
}  // 输出"The last letter of the alphaet."
---
output: The last letter of the alphabet.

在这个例子中,第一个case分支用于匹配第一个英文字母a,第二个case分支用于匹配最后一个字母z。因为switch语句必须有一个case分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以switch语句使用default分支来匹配除了az外的所有值,这个分支保证了switch语句的完备性。

不存在隐式的贯穿

CObjective-C中的switch语句不同,在Swift中,当匹配的case分支中的代码执行完毕后,程序会终止switch语句,而不会继续执行下一个case分支。也就是说,不需要在case分支中显式地使用break语句,这使得switch语句更安全更易用,也避免了漏写break语句导致多个case分支被执行的错误。

注意

虽然在Swift中break不是必须的,但是依然可以在case分支中的代码执行完毕前使用break跳出。

每一个case分支都必须包含至少一条语句。向下面这样书写代码是无效的,因为第一个case分支是空的:

let anotherCharacter: Character = "a"
switch anotherCharacter {
//case "a": // 无效,这个分支下面没有语句,若取消注释则报错'case' label in a 'switch' should have at least one executable statement
case "A":
    print("The letter A.")
default:
    print("Not the letter A.")
}
---
output: Not the letter A.

不像C语言里的switch语句,在Swift中,switch语句不会一起匹配“A”和“a”。避免了意外地从一个case分支贯穿到另外一个,使得代码更安全、也更直观。

为了让单个case同时匹配aA,可以将这两个值组合成一个复合匹配,并且用逗号分开。

let AnotherCharacter: Character = "a"
switch AnotherCharacter {
case "A", "a":
    print("The letter A.")
default:
    print("Not the letter A.")  // 输出"The letter A."
}
---
output: The letter A.

为了可读性,复合匹配可以写成多行形式。

注意

如果想要显式贯穿case分支,请使用fallthrough语句

区间匹配

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 = "servel"
case 12..<100:
    naturalCount = "dozens of"
case 100..<1000:
    naturalCount = "hundreds of"
default:
    naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")  // 输出"There are dozens of Moons orbiting Saturn."
---
output: There are dozens of Moons orbiting Saturn.

在上例中,approximateCount在一个switch声明中被评估。每一个case都与之进行比较。因为approximateCount落到了12和100的区间,所以naturalCount等于“dozens of”值,并且此后的执行跳出了switch语句。

元组

可以使用元组在同一个switch语句中测试多个值,元组中的元素可以是值,也可以是区间。另外,使用下划线_来匹配所有可能的值。

下面的例子展示了如何使用一个(Int, Int)类型的元组类分类下图中的点(x, y):

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    print("\(somePoint) is at the origin.")
case (_, 0):
    print("\(somePoint) is at the x-axis.")
case (0, _):
    print("\(somePoint) is at 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."
---
output: (1, 1) is inside the box.

Coordinate Graph Simple

在上面的例子中,switch语句会判断某个点是否是原点(0, 0),是否在红色的x轴上,是否在橘黄色的y轴上,是否在一个以原点为中心的4x4的蓝色矩形里,或者在这个矩形外面。

不像C语言,Swift允许多个case匹配同一个值。实际上,在这个例子中,点(0, 0)可以匹配所有四个case。但是如果存在多个匹配,那么只会执行第一个被匹配到的case分支。考虑点(0, 0)会首先匹配case(0, 0),因此剩下的能够匹配的分支都会被忽视掉。

值绑定(Value Bindings)

case分支允许将匹配的值声明为临时常量或变量,并且在case分支体内使用,这种行为被称为值绑定(Value Binding),因为匹配的值在case分支体内,与临时的常量或变量绑定。

下面的例子将下图中的点(x, y),使用(Int, Int)类型的元组表示,然后分类表示:

let anotherPoint = (2, 1)
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)).")
}  // Somewhere else at (2, 1).
---
output: Somewhere else at (2, 1).

Coordinate Graph Medium

在上面的例子中,switch语句会判断某个点是否在红色的x轴上,是否在橘黄色的y轴上,或者不在坐标轴上。

这三个case都声明了常量xy的占位符,用于临时获取元组anotherPoint的一个或两个值,第一个case-->case(let x, 0)将匹配一个纵坐标为0的点,并把这个点的横坐标赋值给临时常量x。类似的,第二个case-->case(0, let y)将匹配一个横坐标为0的点,并把这个点的纵坐标赋值给临时常量y

一旦声明了这些临时常量,它们就可以在其对应的case分支里使用,在这个例子中,它们用于打印给定点的类型。

请注意,这个switch语句不包含默认分支,这是因为最后一个case-->case let (x, y)声明了一个可以匹配余下所有值的元组,这使得switch语句已经完备了,因此不需要再书写默认分支。

where

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.")
}  // 输出"(1, -1) is on the line x == -y."
---
output: (1, -1) is on the line x == -y.

Coordinate Graph Complex

在上面的例子中,switch语句会判断某个点是否在绿色的对角线x == y上,是否在紫色的对角线x == -y上,或者不在对角线上。

这三个case都声明了常量xy的占位符,用于临时获取元组yetAnotherPoint的两个值,这两个常量被用作where语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当where语句的条件为true时,匹配到的case分支才会被执行。

就像是值绑定中的例子,由于最后一个case分支匹配了余下所有可能的值,switch语句就已经完备了,因此不需要再书写默认分支。

复合型Cases

当多个条件可以使用同一种方式来处理时,可以将这几种可能放在同一个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) is not a vowel or a consonant.")
}  // 打印输出"e is a vowel."
---
output: e is a vowel.

这个switch语句中的第一个case,匹配了英语中的五个小写元音字母。相似的,第二个case匹配了英语中所有的小写辅音字母。最终,default分支匹配了其它所有字符。

复合匹配同样可以包含值绑定,复合匹配里所有的匹配模式,都必须包含相同的值绑定,并且每一个绑定都必须获取到相同类型的值。这保证了,无论复合匹配的哪个模式发生了匹配,分支体内的代码都能获取到绑定的值,并且绑定的值都有一样的类型。

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

上面的case有两种模式:(let distance, 0)匹配了在x轴上的值,(0, let distance)匹配了在y轴上的值。这两个模式都绑定了distance,并且distance在两种模式下,都是整型。这意味着分支体内的代码,只要case匹配,都可以获取到distance的值。


控制转移语句

控制转移语句改变代码执行的顺序,通过它可以实现代码的跳转。Swift有五种控制转移语句:

  • continue
  • break
  • fallthrough
  • return
  • throw

下面将会介绍continuebreakfallthrough语句。return语句将会在函数章节讨论,throw语句会在错误抛出章节讨论。

Continue

continue语句告诉一个循环体立刻停止本次循环,重新开始下次循环。就好像在说“本次循环我已经执行完了”,但是并不会离开整个循环体。

下面的例子把一个小写字符串中的元音字母和空格字符删除,生成了一个含义模糊的短句:

let puzzleInput = "Great minds think alike"
var puzzleOutput = ""
for character in puzzleInput {
    switch character {
    case "a", "e", "i", "o", "u", " ":
        continue
    default:
        puzzleOutput.append(character)
    }
}
print(puzzleOutput)  // 输出"Grtmndsthnklk"
---
output: Grtmndsthnklk

在上面的代码中,只要匹配到元音字母或者空格字符,就调用continue语句,使本次循环结束,重新开始下次循环。这种行为使switch`匹配到元音字母和空格字符时不做处理,而不是让每一个匹配到的字符都被打印。

Break

break语句会立刻结束整个控制流的执行。break可以在switch或循环语句中使用,用来提前结束switch或循环语句。

循环语句中的Break

当在一个循环体中使用break时,会立刻中断该循环体的执行,然后跳转到表示循环体结束的大括号}后的第一行代码。不会再有本次循环的代码被执行,也不会再有下次的循环产生

Switch语句中的Break

当在一个switch代码块中使用break时,会立刻中断该switch代码块的执行,并且跳转到表示switch代码块结束的大括号}后的第一行代码。

这种特性可以被用来匹配或者忽略一个或多个分支,因为Swiftswitch需要包含所有的分支而且不允许有为空的分支,有时为了使意图更明显,需要特意匹配或者忽略某个分支。那么当想忽略某个分支时,可以在该分支内写上break语句,当那个分支被匹配到时,分支内的break语句立即结束switch代码块。

注意

当一个switch分支仅仅包含注释时,会被报编译时错误。注释不是代码语句而且也不能让switch分支达到被忽略的效果。应该使用break来忽略某个分支。

下面的例子通过switch来判断一个Character值是否代表下面四种语言之一。为了简洁,多个值被包含在了同一个分支情况中。

let numberSymbol: Character = "三"  // 简体中文里的数字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).")  // 输出"The integer value of 三 is 3."
} else {
    print("An integer value could not be found for \(numberSymbol).")
}
---
output: The integer value of  is 3.

这个例子检查numberSymbol是否拉丁,阿拉伯,中文或者泰语中的1到4之一。如果被匹配到,该switch分支语句给Int?类型变量possibleIntegerValue设置一个整数值。

switch代码块执行完成后,接下来的代码通过使用可选绑定来判断possibleIntegerValue是否曾经被设置过值。因为是可选类型的缘故,possibleIntegerValue有一个隐式的初始值nil,所以仅仅当possibleIntegerValue曾被switch代码块的前四个分支中的某一个设置过一个值时,可选的绑定才会被判定为成功。

在上面的例子中,想要把Character所有的可能性都枚举出来是不现实的,所以使用default分支来包含所有上面没有匹配到字符的情况。由于这个default分支不需要执行任何动作,所以只写了一条break语句。一旦落入到default分支中后,break语句就完成了该分支的所有代码操作,代码继续向下,开始执行if let语句。

贯穿(Fallthrough)

Swift里,switch语句不会从上一个case分支跳转到下一个case分支中。相反,只要第一个匹配到的case分支完成了它需要执行的语句,整个switch代码块完成了它的执行。相比之下,C语言要求显式的插入break语句到每个case分支的末尾来阻止自动落入到下一个case分支中。Swift的这种避免默认落入到下一个分支中的特性意味着它的switch功能要比C语言的更加清晰和可预测,可以避免无意识地执行多个case分支从而引发的错误。

如果确实需要C语言风格的贯穿的特性,可以在每个需要该特性的case分支中使用fallthrough关键字,下面的例子使用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)  // 打印输出"The number 5 is a prime number, and also an integer."
---
output: The number 5 is a prime number, and also an integer.

这个例子定义了一个String类型的变量description并且给它设置了一个初始值。函数使用switch逻辑来判断integerToDescribe变量的值。当integerTo Describe的值属于列表中的素数之一时,该函数在description后面添加一段文字,来表明这个数字是一个素数,然后使用fallthrough关键字来“贯穿”到default分支中。default分支在description的最后一段添加额外的文字,至此switch代码块执行完了。

如果integerToDescribe的值不属于列表中的任何素数,那么它不会匹配到第一个switch分支,而这里没有其他特别的分支,因此integerToDescribe匹配到default分支中。

switch代码块执行完成后,使用print(_:separator:terminator:)函数打印该数字的描述,在这个例子中,数字5被准确地识别为了一个素数。

注意

fallthrough关键字不会检查它下一个将会落入执行的case中的匹配条件,fallthrough简单地使代码继续连接到下一个case代码中,这和C语言标准中的switch语句特性是一样的。

带标签的语句

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

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

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

/*
 label name: while condition {
     statements
 }
 */

下面的例子是前面章节中🐍和🪜的适配版本,在此版本中,将使用一个带有标签的while循环体调用breakcontinue语句。这次,游戏增加了一个额外的规则:

  • 为了获胜,必须刚好落在第25个方格中
  • 如果某次掷骰子使得移动超出第25个方块,必须重新掷骰子,直到掷出的骰子数刚好能使得落在第25个方块中。游戏的棋盘和之前的一样。

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:
        break gameLoop
    case let newSquare where newSquare > finalSQUARE:
        continue gameLoop
    default:
        SQUARE += diceROLL
        SQUARE += BOARD[SQUARE]
    }
    print(SQUARE, terminator: " ")
}
print("Game Over!")
---
output: 1 11 4 8 13 8 18 20 23 16 18 21 Game Over!

每次循环迭代开始时掷骰子。与之前玩家掷完骰子就立即移动不同,这里使用了switch语句来考虑每次移动可能产生的结果,从而决定玩家本次是否能够移动。

  • 如果骰子数刚好使玩家移动到最终的方格里,游戏结束。break gameLoop语句跳转控制去执行while循环体后的第一行代码,意味着游戏结束。
  • 如果骰子数将会使玩家的移动超出最后的方格,那么这种移动是不合法的,玩家需要重新掷骰子。continue gameLoop语句结束本次while循环,开始下一次循环。
  • 在剩余的所有情况中,骰子数产生的都是合法的移动。玩家向前移动diceROLL个方格,然后游戏逻辑再处理玩家当前是否处于蛇头或者梯子的底部。接着本次循环结束,控制跳转到while循环体的条件判断语句处,再决定是否需要继续执行下次循环。

注意

如果上述的break语句没有使用gameLoop标签,那么它将会中断switch语句而不是while循环。使用gameLoop标签清晰的表明了break想要中断的是哪个代码块。

同时请注意,当调用continue gameLoop去跳转到下一次循环迭代时,这里使用gameLoop标签并不是严格必须的。因为在这个游戏中,只有一个循环体,所以continue语句会影响到哪个循环体是没有歧义的。然而continue语句使用gameLoop标签也是没有危害的。这样做符合标签的使用规则,同时参照旁边的break gameLoop,能够使游戏的逻辑更加清晰和易于理解。


提前退出

if语句一样,guard的执行取决于一个布尔表达式的布尔值。我们可以使用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"])  // 输出"Hello John!"  // 输出"I hope the weather is nice near you."
greet(person: ["name": "Jensen", "location": "Hefei"])
---
output: Hello John!
I hope the weather is nice near you.

如果guard语句的条件被满足,则继续执行guard语句大括号后的代码。将变量或者常量的可选绑定作为guard语句的条件,都可以保护guard语句后面的代码。

如果条件不被满足,在else分支上的代码就会被执行。这个分支必须转移控制以退出guard语句出现的代码段。它可以用控制转移语句如returnbreakcontinue或者throw做这件事,或者调用一个不返回的方法或者函数,例如fatalError()

相比于可以实现同样功能的if语句,按需要使用guard语句会提升代码的可读性。它可以使代码连贯执行而不需要将它们包含在else块中,它可以使你在紧邻条件判断的地方,处理违规的情况。


检测API可用性

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

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

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

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

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

在它一般的形式中,可用性条件使用了一个平台名字和版本的列表。平台名字可以是iOSmacOSwatchOStvOS,除了指定像iOS 8macOS 10.10的大版本号,也可以指定像iOS 11.2.6以及macOS 10.13.3的小版本号。

/*
if #available(平台名称 版本号, ..., *) {
     APIs 可用,语句将执行
 } else {
     APIs 不可用,语句将不执行
 }
 */
Last updated on