今天我们一起来学习循环。循环是一个好东西,它使得计算机称为如此出色的工具,它利用了计算机的速度每秒重复运行着简单的任务数十亿次而不知疲惫。
不过,循环虽然是一个好东西,它很有用,但如果处理不当,它也是造成计算机死机的罪魁祸首。这到底是怎么回事,让我们今天一起来一探究竟。
如何使用 for 循环重复工作
计算机非常擅长做重复性工作,而 Swift 可以轻松地重复某些代码固定的次数,或者对数组、字典或集合中的每个项目重复一次。
让我们从简单的事情开始:如果我们有一个字符串数组,我们可以像这样打印每个字符串:
let platforms = ["iOS", "macOS", "tvOS", "watchOS"]
for os in platforms {
print("Swift works great on \(os).")
}
这将循环遍历 platforms
中的所有项目,并将它们逐一放入 os
中。我们没有在其他地方创建 os
;它是作为循环的一部分为我们创建的,并且只在开头和结尾的大括号内可用。
括号内是我们要为数组中的每个项运行的代码,因此上面的代码将打印四行,每个循环项一行。首先,它将 “iOS”放在其中并调用 print()
,然后将 “macOS”放在其中并调用 print()
,接着是 “tvOS”,最后是 “watchOS”。
为了便于理解,我们给这些东西起了共同的名字:
- 我们将大括号内的代码称为循环体
- 我们称循环体的一次循环为循环迭代。
- 我们称
os
为循环变量。它只存在于循环体中,并将在下一次循环迭代中改变为新值。
应该说,os
这个名字并不特别,我们可以这样写:
for name in platforms {
print("Swift works great on \(name).")
}
甚至这样写:
for androidOS in platforms {
print("Swift works great on \(androidOS).")
}
代码仍然可以完全一样地工作。
事实上,Xcode 在这方面真的很聪明:如果你写的是 for plat
,它会识别出有一个名为 platforms
的数组,并自动完成 for platform in platforms
——它会识别出 platforms 是复数,并建议循环变量使用单数名称。当看到 Xcode 的建议出现时,按 Return 键选择它。
你也可以在一个固定的数字范围内循环,而不是在一个数组(或集合,或字典——语法相同!)上循环。例如,我们可以像这样打印出从 1 到 12 的 5 倍表:
for i in 1...12 {
print("5 x \(i) is \(5 * i)")
}
其中有几处是新知识,让我们暂停一下,仔细研究一下:
- 我使用了循环变量
i
,这是 “正在计数的数字 “的常用编码约定。如果你在数第二个数字,你会使用j
,如果你在数第三个数字,你会使用k
,但如果你在数第四个数字,也许你应该选择更好的变量名。 1...12
部分是一个范围,意思是 “介于 1 和 12 之间的所有整数,以及 1 和 12 本身”。在 Swift 中,范围是一种独特的数据类型。
因此,当循环第一次运行时,i 将是 1,然后是 2、3 等,一直到 12,之后循环结束。
你还可以将循环放在循环内部,称为嵌套循环,就像下面这样:
for i in 1...12 {
print("The \(i) times table:")
for j in 1...12 {
print(" \(j) x \(i) is \(j * i)")
}
print()
}
这展示了其他一些新东西,让我们再次暂停并仔细观察一下:
- 现在有一个嵌套循环:我们从 1 数到 12,里面的每个数字我们再从 1 数到 12。
- 单独使用
print()
,在没有输入文本或数值的情况下,只会开始一行新的内容。这有助于分割我们的输出,使其在屏幕上看起来更美观。
因此,当你看到 x...y
时,你就会知道它创建了一个范围,从 x
开始,一直数到 y
。
Swift 有一种类似但不同的范围类型,它可以数到但不包括最后一个数字:..<
。这在代码中表现得淋漓尽致:
for i in 1...5 {
print("Counting from 1 through 5: \(i)")
}
print()
for i in 1..<5 {
print("Counting 1 up to 5: \(i)")
}
运行时,在第一个循环中会打印数字 1、2、3、4、5,但在第二个循环中只会打印数字 1、2、3 和 4。我将 1...5
发音为 “一到包含五”,将 1..<5
发音为 “一到不包含五”,在 Swift 的其他地方你也会看到类似的措辞。
提示:**..<
**对于处理数组很有帮助,在数组中,我们从 0 开始计数,并经常希望数到但不包括数组中的项数。
在说完 for
循环之前,我还想说一件事:有时候,你想使用一个范围运行一定次数的代码,但实际上你并不想要循环变量——你不想要 i
或**j
**,因为你并不使用它。
在这种情况下,你可以用下划线代替循环变量,就像这样:
var lyric = "Haters gonna"
for _ in 1...5 {
lyric += " hate"
}
print(lyric)
Swift 为什么在循环中使用下划线?
如果要循环遍历数组中的项目,可以编写这样的代码:
let names = ["Sterling", "Cyril", "Lana", "Ray", "Pam"]
for name in names {
print("\(name) is a secret agent")
}
每次循环时,Swift 都会从 names
数组中读取一项,将其放入 name
常量,然后执行循环的正文——这就是 print()
方法。
不过,有时你实际上并不需要当前读取的值,这就是下划线的作用所在: Swift 会识别出你实际上并不需要这个变量,因此不会为你创建临时常量。
当然,Swift 也能看到这一点——它能看到你是否在循环中使用了 name
,所以不用下划线也能完成同样的工作。不过,使用下划线对我们的大脑也有类似的作用:我们可以查看代码,并立即发现循环变量没有被使用,无论循环体内部有多少行代码。
因此,如果你没有在循环体中使用循环变量,Swift 会警告你像这样重写:
let names = ["Sterling", "Cyril", "Lana", "Ray", "Pam"]
for _ in names {
print("[CENSORED] is a secret agent!")
}
为什么Swift有两个范围操作?
当我们考虑数值的范围时,语言是相当令人困惑的。如果我说 “给我截至昨天的销售数据”,是指包括昨天还是不包括昨天?两者都有各自的用处,因此 Swift 提供了一种同时表示两者的方法:..<
是半开放范围,表示 “截至但不包括”;**...
**是封闭范围操作符,表示 “截至并包括”。
为了在交谈时更容易区分,Swift 经常使用非常具体的语言: “1 to 5 “表示 1、2、3 和 4,而 “1 through 5 “表示 1、2、3、4 和 5。如果你还记得,Swift 的数组从索引 0 开始,这意味着一个包含三个项的数组的索引分别为 0、1 和 2,这是半开范围操作符的完美用例。
当你只想要某个范围的一部分时,情况就变得更有趣了,比如 “从 0 开始的任何内容 “或 “索引 5 到数组的末尾”。你看,这些在编程中相当有用,所以 Swift 让我们只指定范围的一部分,从而使它们更容易创建。
例如,如果我们有这样一个名称数组:
let names = ["Piper", "Alex", "Suzanne", "Gloria"]
我们可以这样读出一个人的名字:
print(names[0])
通过范围,我们还可以打印出类似这样的数值范围:
print(names[1...3])
不过,这样做有一个小风险:如果我们的数组并不不包含四个项目,那么 1…3 就会失败。幸运的是,我们可以使用单边范围来表示 “给我 1 到数组的末尾”,就像这样:
print(names[1...])
因此,范围非常适合对特定值进行计数,但也有助于从数组中读取项目组。
如何使用 while 循环重复工作
Swift 还有一种名为 while
的循环:提供一个条件,while
循环就会持续执行循环体,直到条件为假。
虽然你仍会不时看到 while
循环,但它们并不像 for
循环那样常见。因此,我想介绍一下它们,让你知道它们的存在,但我们不要在它们身上花费太久时间。
下面是一个基本的 while
循环,让我们开始学习:
var countdown = 10
while countdown > 0 {
print("\(countdown)…")
countdown -= 1
}
print("Blast off!")
这样,循环体——打印数字并减去 1——将持续运行,直到倒计数等于或小于 0,此时循环结束并打印最终信息。
当你不知道循环将运行多少次时,while
循环就非常有用。为了演示这一点,我想向你介绍 Int
和 Double
都有的一个非常有用的功能:random(in:)
。给它一个数字范围,它就会在这个范围内的某个地方随机返回一个 Int
或 Double
。
例如,以下代码会创建一个介于 1 和 1000 之间的新整数:
let id = Int.random(in: 1...1000)
这样就会随机产生一个介于 0 和 1 之间的小数:
let amount = Double.random(in: 0...1)
我们可以将此功能与 while
循环结合使用,反复掷一些虚拟的 20 面骰子,只有当掷出 20 时才结束循环。
下面是实现这一功能的代码:
// create an integer to store our roll
var roll = 0
// carry on looping until we reach 20
while roll != 20 {
// roll a new dice and print what it was
roll = Int.random(in: 1...20)
print("I rolled a \(roll)")
}
// if we're here it means the loop ended – we got a 20!
print("Critical hit!")
当你不知道在一个循环中应该使用**for
还是while
时,你可以这样判断:当你知道循环的范围或次数时,一般都是使用for
循环;当你不知道循环需要进行多少次,只知道它何时(什么条件)会停下来时,你就应该使用while
**。
何时使用 while 循环?
Swift 为我们提供了 for
循环和 while
循环,这两种循环都很常用。然而,在刚开始学习时,有两种常用的循环方式似乎很奇怪——你应该使用哪一种,为什么?
主要区别在于,for
循环一般用于有限序列:例如,我们循环数字 1 到 10,或者循环数组中的项目。另一方面,while
循环可以循环到任意条件变为 false 为止,直到我们告诉它们停止为止。
这意味着我们可以重复相同的代码,直到…
- …用户要求我们停止
- …服务器要求我们停止
- …我们找到了想要的答案
- …我们生成了足够多的数据
以上只是一些例子。想想看:如果我让你掷 10 次骰子并打印结果,你可以用一个简单的 for 循环从 1 数到 10 来完成。但如果我让你掷 10 个骰子并打印结果,同时在前一个骰子结果相同时自动再掷另一个骰子,那么你就无法提前知道需要掷多少个骰子。也许你会很幸运,只需要掷 10 次骰子,但也许你会重复掷几次,需要掷 15 次骰子。又或者你会遇到很多重复掷骰子的情况,需要掷 30 次——谁知道呢?
这时 while
循环就派上用场了:我们可以一直循环下去,直到准备好退出。
如何使用 break 和 continue 跳过循环项
Swift 提供了两种跳过循环中一个或多个项目的方法:continue
跳过当前循环迭代,break
跳过所有剩余迭代。与 while
循环一样,这两种方法有时也会用到,但在实际应用中却比想象的要少得多。
让我们从 continue
开始逐一了解它们。在对数据数组进行循环时,Swift 会从数组中取出一个项目,并使用它执行循环体。如果在循环体中调用 continue
,Swift 会立即停止执行当前的循环迭代,并跳转到循环中的下一个项目,然后继续正常执行。这通常用在循环的起始位置,在这里你可以消除那些没有通过你选择的测试的循环变量。
下面是一个例子:
let filenames = ["me.jpg", "work.txt", "sophie.jpg", "logo.psd"]
for filename in filenames {
if filename.hasSuffix(".jpg") == false {
continue
}
print("Found picture: \(filename)")
}
它会创建一个文件名字符串数组,然后循环遍历每一个文件名字符串,并检查其后缀是否为”.jpg”——即是否是一张图片。继续用于所有未通过该测试的文件名,这样就跳过了循环体的其余部分。
至于 break
,它会立即退出循环,并跳过所有剩余的迭代。为了演示这一点,我们可以编写一些代码来计算两个数字的 10 个公倍数:
let number1 = 4
let number2 = 14
var multiples = [Int]()
for i in 1...100_000 {
if i.isMultiple(of: number1) && i.isMultiple(of: number2) {
multiples.append(i)
if multiples.count == 10 {
break
}
}
}
print(multiples)
这样就可以做很多事情了:
- 创建两个常量来保存两个数字。
- 创建一个整数数组变量,用于存储两个数字的公倍数。
- 从 1 数到 100000,将每个循环变量赋值给 i。
- 如果 i 是第一个和第二个数字的倍数,则将其追加到整数数组中。
- 一旦数到 10 个倍数,就调用 break 退出循环。
- 打印出结果数组。
因此,当你想跳过当前循环迭代的剩余部分时,请使用 continue
;当你想跳过所有剩余的循环迭代时,请使用 break
。
为什么要退出循环?
Swift 的 break 关键字可以让我们立即退出循环,无论我们在讨论哪种循环。很多时候你并不需要这样做,因为你正在循环数组中的项,并希望处理所有这些项,或者因为你正在从 1 数到 10,并希望处理所有这些值。
不过,有时你确实希望提前结束循环。例如,如果您有一个分数数组,并想知道玩家取得了多少分数而没有得到 0,你可以这样写:
let scores = [1, 8, 4, 3, 0, 5, 2]
var count = 0
for score in scores {
if score == 0 {
break
}
count += 1
}
print("You had \(count) scores before you got 0.")
如果不中断,即使找到了第一个 0,我们也需要继续循环分数,这是一种浪费。
Swift为什么会有贴标签的声明?
Swift 的标签语句允许我们为代码的某些部分命名,最常用于打破嵌套循环。
为了演示标签语句,让我们来看一个例子:我们试图找出打开保险箱的正确动作组合。我们可以先定义一个包含所有可能动作的数组:
let options = ["up", "down", "left", "right"]
为便于测试,以下是我们要猜测的秘密组合:
let secretCombination = ["up", "up", "right"]
- 要找到这种组合,我们需要制作包含所有可能的三色变量的数组:
- 上、上、上
- 上、上、下
- 上、上、左
- 上、上、右
- 上、下、左
- 上、下、右
……你明白了吧。
要做到这一点,我们可以写三个循环,一个嵌套在另一个里面,就像这样:
for option1 in options {
for option2 in options {
for option3 in options {
print("In loop")
let attempt = [option1, option2, option3]
if attempt == secretCombination {
print("The combination is \(attempt)!")
}
}
}
}
这样就可以多次重复相同的项目,从而创建一个尝试数组,如果尝试结果与秘密组合匹配,就会打印出一条信息。
但这段代码有一个问题:一旦我们找到了组合,循环就结束了,为什么循环还要继续运行呢?我们真正想说的是 “一旦找到组合,立即退出所有循环”——这就是标签语句的作用所在。我们可以用标签语句来写:
outerLoop: for option1 in options {
for option2 in options {
for option3 in options {
print("In loop")
let attempt = [option1, option2, option3]
if attempt == secretCombination {
print("The combination is \(attempt)!")
break outerLoop
}
}
}
}
有了这个小改动,一旦找到组合,这三个循环就会停止运行。在这种微不足道的情况下,性能差别不大,但如果你的项目有数百甚至数千个呢?保存这样的工作是个好主意,值得你为自己的代码牢记。
何时使用break,何时使用continue
有时,Swift 学习者很难理解什么时候使用 break 关键字是正确的,什么时候使用 continue 关键字是正确的,因为它们都会改变我们的循环流程。
当我们使用 continue 关键字时,我们是在说 “我已经完成了这个循环的当前运行”——Swift 会跳过循环体的其余部分,转到循环中的下一个项目。但当我们说 break 时,我们是在说 “我已经完全结束了这个循环,所以请完全退出”。这意味着 Swift 将跳过循环体的剩余部分,同时也跳过任何其他循环项。
就像 break 一样,如果你愿意,也可以在带标签的语句中使用 continue,但老实说,我不记得见过有人这么做!
总结:条件和循环
在前面的章节中,我们已经介绍了很多关于条件和循环的内容,现在让我们来回顾一下:
- 我们使用 if 语句来检查条件是否为真。你可以输入任何你想要的条件,但最终必须归结为布尔值。
- 如果需要,可以添加 else 块和/或多个 else if 块来检查其他条件。Swift 会按顺序执行这些代码。
- 你可以使用 || 组合条件,这意味着如果其中一个子条件为真,则整个条件为真;或者使用 &&,这意味着如果两个子条件都为真,则整个条件为真。
- 如果经常重复同类检查,可以使用 switch 语句来代替。这些语句必须始终穷举,这可能意味着要添加默认情况。
- 如果其中一个 switch 用到了 fallthrough,这意味着 Swift 会在之后执行下面的 case。这种情况并不常用。
- 三元条件操作符让我们可以检查 WTF: 什么、真、假。虽然一开始读起来有点困难,但在 SwiftUI 中你会看到它被频繁使用。
- for 循环让我们可以对数组、集合、字典和范围进行循环。你可以将项目分配给循环变量并在循环中使用,也可以使用下划线 _ 忽略循环变量。
- while 循环让我们可以制作自定义循环,持续运行直到某个条件变为 false。
- 我们可以分别使用 continue 或 break 跳过部分或全部循环项。
这又是一大段新内容,但有了条件和循环,我们现在已经掌握了足够的知识来构建一些非常有用的软件——让我们试试看吧!
思考题
编写一个playground小程序,筛选1到100,对于每个数字:
- 如果是3的倍数,请打印“3的倍数”
- 如果是5的倍数,请打印“5的倍数”
- 如果是3和5的公倍数,请打印“3和5的公倍数”
- 其他的数字只需打印数字即可
代码的解法可能不止一种,大家不用纠结正确答案,只要结果正确即可。
下一章,我们开始学习函数。