简介
欢迎来到「控制流」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将重点关注 Rust 中的控制流,它涉及使用 if 表达式和循环,根据条件运行代码,并在条件为真时重复代码。
欢迎来到「控制流」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将重点关注 Rust 中的控制流,它涉及使用 if 表达式和循环,根据条件运行代码,并在条件为真时重复代码。
根据条件是否为 true 来运行某些代码,以及在条件为 true 时重复运行某些代码的能力,是大多数编程语言的基本构建块。用于控制 Rust 代码执行流程的最常见结构是 if 表达式和循环。
if 表达式if 表达式允许你根据条件来使代码分支。你提供一个条件,然后声明:“如果满足这个条件,就运行这段代码块。如果条件不满足,就不运行这段代码块。”
在你的 project 目录下创建一个名为 branches 的新项目,来探索 if 表达式。在 src/main.rs 文件中,输入以下内容:
cd ~/project
cargo new branches
文件名:src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("条件为真");
} else {
println!("条件为假");
}
}
所有的 if 表达式都以关键字 if 开头,后面跟着一个条件。在这个例子中,条件检查变量 number 的值是否小于 5。我们将在条件为 true 时要执行的代码块放在条件后面的花括号内。与 if 表达式中的条件相关联的代码块有时被称为 分支,就像我们在“将猜测与秘密数字进行比较”中讨论的 match 表达式中的分支一样。
可选地,我们也可以包含一个 else 表达式,我们在这里选择这样做,以便在条件计算为 false 时,给程序提供一个可执行的替代代码块。如果你不提供 else 表达式,并且条件为 false,程序将直接跳过 if 块,继续执行下一段代码。
尝试运行这段代码;你应该会看到以下输出:
$ cargo run
正在编译 branches v0.1.0 (file:///projects/branches)
已完成开发 [未优化 + 调试信息] 目标,耗时 0.31 秒
正在运行 `target/debug/branches`
条件为真
让我们尝试将 number 的值更改为使条件为 false 的值,看看会发生什么:
let number = 7;
再次运行程序,并查看输出:
$ cargo run
正在编译 branches v0.1.0 (file:///projects/branches)
已完成开发 [未优化 + 调试信息] 目标,耗时 0.31 秒
正在运行 `target/debug/branches`
条件为假
还值得注意的是,这段代码中的条件 必须 是一个 bool 类型。如果条件不是 bool 类型,我们将会得到一个错误。例如,尝试运行以下代码:
文件名:src/main.rs
fn main() {
let number = 3;
if number {
println!("数字是三");
}
}
这次 if 条件计算结果为 3,Rust 会抛出一个错误:
$ cargo run
正在编译 branches v0.1.0 (file:///projects/branches)
错误[E0308]:类型不匹配
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ 期望 `bool`,找到整数
该错误表明 Rust 期望一个 bool 类型,但得到的是一个整数。与 Ruby 和 JavaScript 等语言不同,Rust 不会自动尝试将非布尔类型转换为布尔类型。你必须明确地始终为 if 提供一个布尔值作为其条件。例如,如果我们希望 if 代码块仅在数字不等于 0 时运行,我们可以将 if 表达式更改为以下内容:
文件名:src/main.rs
fn main() {
let number = 3;
if number!= 0 {
println!("数字不是零");
}
}
运行这段代码将打印出“数字不是零”。
else if 处理多个条件你可以通过在 else if 表达式中组合 if 和 else 来使用多个条件。例如:
文件名:src/main.rs
fn main() {
let number = 6;
if number % 4 == 0 {
println!("数字能被 4 整除");
} else if number % 3 == 0 {
println!("数字能被 3 整除");
} else if number % 2 == 0 {
println!("数字能被 2 整除");
} else {
println!("数字不能被 4、3 或 2 整除");
}
}
这个程序有四种可能的执行路径。运行它之后,你应该会看到以下输出:
$ cargo run
正在编译 branches v0.1.0 (file:///projects/branches)
已完成开发 [未优化 + 调试信息] 目标,耗时 0.31 秒
正在运行 `target/debug/branches`
数字能被 3 整除
当这个程序执行时,它会依次检查每个 if 表达式,并执行条件计算结果为 true 的第一个代码块。请注意,尽管 6 能被 2 整除,但我们没有看到输出“数字能被 2 整除”,也没有看到 else 块中的“数字不能被 4、3 或 2 整除”文本。这是因为 Rust 只执行第一个 true 条件的代码块,一旦找到一个,它甚至不会检查其余的条件。
使用过多的 else if 表达式会使你的代码变得杂乱,所以如果你有多个 else if 表达式,你可能需要重构你的代码。第 6 章介绍了一种强大的 Rust 分支结构,称为 match,适用于这些情况。
let 语句中使用 if因为 if 是一个表达式,所以我们可以在 let 语句的右侧使用它,将结果赋给一个变量,如清单 3-2 所示。
文件名:src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("number 的值是:{number}");
}
清单 3-2:将 if 表达式的结果赋给一个变量
number 变量将根据 if 表达式的结果绑定到一个值。运行这段代码,看看会发生什么:
$ cargo run
正在编译 branches v0.1.0 (file:///projects/branches)
已完成开发 [未优化 + 调试信息] 目标,耗时 0.30 秒
正在运行 `target/debug/branches`
number 的值是: 5
记住,代码块计算结果为其中的最后一个表达式,数字本身也是表达式。在这种情况下,整个 if 表达式的值取决于执行哪个代码块。这意味着 if 每个分支可能产生的结果值必须是相同的类型;在清单 3-2 中,if 分支和 else 分支的结果都是 i32 整数。如果类型不匹配,如下例所示,我们将会得到一个错误:
文件名:src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("number 的值是:{number}");
}
当我们尝试编译这段代码时,将会得到一个错误。if 和 else 分支的值类型不兼容,Rust 会准确指出程序中的问题所在:
$ cargo run
正在编译 branches v0.1.0 (file:///projects/branches)
错误[E0308]:`if` 和 `else` 具有不兼容的类型
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ 期望整数,找到
`&str`
| |
| 因为这个原因期望
if 块中的表达式计算结果为一个整数,而 else 块中的表达式计算结果为一个字符串。这行不通,因为变量必须具有单一类型,并且 Rust 需要在编译时确切知道 number 变量是什么类型。知道 number 的类型可以让编译器在我们使用 number 的任何地方验证类型是否有效。如果 number 的类型仅在运行时确定,Rust 将无法做到这一点;如果编译器必须为任何变量跟踪多个假设类型,那么编译器将会更复杂,并且对代码的保证也会更少。
多次执行一段代码通常很有用。为此,Rust 提供了几种 循环,它们会遍历循环体中的代码直到结束,然后立即从开头重新开始。为了试验循环,让我们创建一个名为 loops 的新项目。
Rust 有三种循环:loop、while 和 for。让我们逐一尝试。
loop 重复代码loop 关键字告诉 Rust 永远重复执行一段代码,或者直到你明确告诉它停止。
例如,将你 loops 目录下的 src/main.rs 文件修改为如下内容:
文件名:src/main.rs
fn main() {
loop {
println!("再来一次!");
}
}
当我们运行这个程序时,我们会看到“再来一次!”被不断重复打印,直到我们手动停止程序。大多数终端支持使用键盘快捷键 ctrl-C 来中断陷入无限循环的程序。试试看:
$ cargo run
正在编译 loops v0.1.0 (file:///projects/loops)
已完成开发 [未优化 + 调试信息] 目标,耗时 0.29 秒
正在运行 `target/debug/loops`
再来一次!
再来一次!
再来一次!
再来一次!
^C再来一次!
符号 ^C 表示你按下 ctrl-C 的位置。根据接收到中断信号时代码在循环中的位置,你可能会也可能不会在 ^C 之后看到“再来一次!”这个词被打印出来。
幸运的是,Rust 还提供了一种使用代码跳出循环的方法。你可以在循环中放置 break 关键字,告诉程序何时停止执行循环。回想一下,我们在“猜对后退出”的猜数字游戏中就是这样做的,当用户猜对正确数字赢得游戏时退出程序。
我们在猜数字游戏中还使用了 continue,它在循环中告诉程序跳过本次循环的任何剩余代码,进入下一次循环。
loop 的用途之一是重试你知道可能会失败的操作,比如检查一个线程是否完成了它的任务。你可能还需要将该操作的结果从循环传递到代码的其余部分。要做到这一点,你可以在用于停止循环的 break 表达式之后添加你想要返回的值;该值将从循环中返回,这样你就可以使用它了,如下所示:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("结果是 {result}");
}
在循环之前,我们声明一个名为 counter 的变量,并将其初始化为 0。然后我们声明一个名为 result 的变量来保存从循环中返回的值。在循环的每次迭代中,我们将 counter 变量加 1,然后检查 counter 是否等于 10。当等于 10 时,我们使用带有值 counter * 2 的 break 关键字。在循环之后,我们使用分号来结束将值赋给 result 的语句。最后,我们打印 result 中的值,在这种情况下是 20。
如果你有嵌套循环,break 和 continue 此时应用于最内层循环。你可以选择为一个循环指定一个 循环标签,然后将其与 break 或 continue 一起使用,以指定这些关键字应用于带标签的循环,而不是最内层循环。循环标签必须以单引号开头。下面是一个有两个嵌套循环的示例:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
外层循环有标签 'counting_up,它将从 0 递增到 2。没有标签的内层循环从 10 递减到 9。第一个没有指定标签的 break 将仅退出内层循环。break 'counting_up; 语句将退出外层循环。这段代码打印:
正在编译 loops v0.1.0 (file:///projects/loops)
已完成开发 [未优化 + 调试信息] 目标,耗时 0.58 秒
正在运行 `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
while 进行条件循环程序常常需要在循环中评估一个条件。只要条件为 true,循环就会运行。当条件不再为 true 时,程序会调用 break 来停止循环。你可以使用 loop、if、else 和 break 的组合来实现这样的行为;如果你愿意的话,可以现在就在一个程序中尝试一下。然而,这种模式非常常见,以至于 Rust 为此提供了一种内置的语言结构,称为 while 循环。在清单 3-3 中,我们使用 while 让程序循环三次,每次递减计数,然后在循环结束后打印一条消息并退出。
文件名:src/main.rs
fn main() {
let mut number = 3;
while number!= 0 {
println!("{number}!");
number -= 1;
}
println!("发射!!!");
}
清单 3-3:使用 while 循环在条件求值为 true 时运行代码
这种结构消除了使用 loop、if、else 和 break 时所需的大量嵌套,并且更清晰。只要条件求值为 true,代码就会运行;否则,它会退出循环。
for 循环遍历集合你可以选择使用 while 结构来遍历集合中的元素,比如数组。例如,清单 3-4 中的循环打印了数组 a 中的每个元素。
文件名:src/main.rs
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("值是:{}", a[index]);
index += 1;
}
}
清单 3-4:使用 while 循环遍历集合中的每个元素
在这里,代码按顺序遍历数组中的元素。它从索引 0 开始,然后循环直到到达数组中的最后一个索引(也就是说,当 index < 5 不再为 true 时)。运行这段代码将打印数组中的每个元素:
$ cargo run
正在编译 loops v0.1.0 (file:///projects/loops)
已完成开发 [未优化 + 调试信息] 目标,耗时 0.32 秒
正在运行 `target/debug/loops`
值是: 10
值是: 20
值是: 30
值是: 40
值是: 50
正如预期的那样,所有五个数组值都出现在终端中。即使 index 在某个时候会达到值 5,循环也会在尝试从数组中获取第六个值之前停止执行。
然而,这种方法容易出错;如果索引值或测试条件不正确,我们可能会导致程序恐慌。例如,如果你将 a 数组的定义改为有四个元素,但忘记将条件更新为 while index < 4,代码就会恐慌。它也很慢,因为编译器会添加运行时代码,以便在每次循环迭代时执行索引是否在数组边界内的条件检查。
作为一种更简洁的替代方法,你可以使用 for 循环,并为集合中的每个项执行一些代码。for 循环看起来像清单 3-5 中的代码。
文件名:src/main.rs
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("值是:{element}");
}
}
清单 3-5:使用 for 循环遍历集合中的每个元素
当我们运行这段代码时,我们将看到与清单 3-4 相同的输出。更重要的是,我们现在提高了代码的安全性,并消除了因超出数组末尾或未遍历足够远而遗漏一些项可能导致的错误。
使用 for 循环,如果改变数组中的值的数量,你不需要像在清单 3-4 中使用的方法那样记住更改任何其他代码。
for 循环的安全性和简洁性使其成为 Rust 中最常用的循环结构。即使在你想要像清单 3-3 中使用 while 循环的倒计时示例那样运行某些代码特定次数的情况下,大多数 Rust 开发者也会使用 for 循环。实现方法是使用标准库提供的 Range,它会生成从一个数字开始并在另一个数字之前结束的所有连续数字。
下面是使用 for 循环和我们尚未讨论过的另一种方法 rev 来反转范围的倒计时代码:
文件名:src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("发射!!!");
}
这段代码是不是更好一些?
恭喜你!你已经完成了控制流实验。你可以在 LabEx 中练习更多实验来提升你的技能。