控制流

在大多数编程语言中,根据条件是否为 true 来运行某些代码,以及在条件为 true 时重复运行某些代码的能力,都是基本的构建块。最常见的控制 Rust 代码执行流程的结构是 if 表达式和循环。

if 表达式

if 表达式允许你根据条件分支你的代码。你提供一个条件,然后声明:“如果满足此条件,则运行此代码块。如果条件不满足,则不运行此代码块。”

在你的 projects 目录中创建一个名为 branches 的新项目,以探索 if 表达式。在 src/main.rs 文件中,输入以下内容

文件名:src/main.rs

fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } }

所有 if 表达式都以关键字 if 开头,后跟一个条件。在本例中,条件检查变量 number 的值是否小于 5。我们将条件为 true 时要执行的代码块放在条件之后的花括号内。与 if 表达式中的条件关联的代码块有时称为分支,就像我们在“将猜测与秘密数字比较”中讨论的 match 表达式中的分支一样第 2 章的章节。

可选地,我们还可以包含一个 else 表达式,我们在这里选择了这样做,以在条件评估为 false 时为程序提供一个替代代码块来执行。如果你不提供 else 表达式且条件为 false,则程序将跳过 if 代码块并继续执行下一段代码。

尝试运行这段代码;你应该看到以下输出

$ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s Running `target/debug/branches` condition was true

让我们尝试将 number 的值更改为使条件为 false 的值,看看会发生什么

fn main() { let number = 7; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } }

再次运行程序,并查看输出

$ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s Running `target/debug/branches` condition was false

还值得注意的是,此代码中的条件必须bool 类型。如果条件不是 bool 类型,我们将收到错误。例如,尝试运行以下代码

文件名:src/main.rs

fn main() { let number = 3; if number { println!("number was three"); } }

这次 if 条件评估为值 3,Rust 抛出一个错误

$ cargo run Compiling branches v0.1.0 (file:///projects/branches) error[E0308]: mismatched types --> src/main.rs:4:8 | 4 | if number { | ^^^^^^ expected `bool`, found integer For more information about this error, try `rustc --explain E0308`. error: could not compile `branches` (bin "branches") due to 1 previous error

该错误表明 Rust 期望得到一个 bool 类型,但却得到了一个整数。与 Ruby 和 JavaScript 等语言不同,Rust 不会自动尝试将非布尔类型转换为布尔类型。你必须显式地始终为 if 提供一个布尔值作为其条件。例如,如果我们希望 if 代码块仅在数字不等于 0 时运行,我们可以将 if 表达式更改为以下内容

文件名:src/main.rs

fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); } }

运行此代码将打印 number was something other than zero

使用 else if 处理多个条件

你可以通过在 else if 表达式中组合 ifelse 来使用多个条件。例如

文件名:src/main.rs

fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else if number % 2 == 0 { println!("number is divisible by 2"); } else { println!("number is not divisible by 4, 3, or 2"); } }

这个程序有四个可能的执行路径。运行它后,你应该看到以下输出

$ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s Running `target/debug/branches` number is divisible by 3

当这个程序执行时,它会依次检查每个 if 表达式,并执行条件评估为 true 的第一个主体。请注意,即使 6 可以被 2 整除,我们也没有看到输出 number is divisible by 2,也没有看到来自 else 代码块的 number is not divisible by 4, 3, or 2 文本。那是因为 Rust 只执行第一个 true 条件的代码块,一旦找到一个,它甚至不会检查其余的条件。

使用过多的 else if 表达式会使你的代码变得混乱,因此如果你有多个,你可能需要重构你的代码。第 6 章描述了一个强大的 Rust 分支结构,称为 match,用于这些情况。

let 语句中使用 if

因为 if 是一个表达式,所以我们可以将其用在 let 语句的右侧,以将结果赋值给一个变量,如 Listing 3-2 所示。

文件名:src/main.rs
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
Listing 3-2:将 if 表达式的结果赋值给变量

number 变量将根据 if 表达式的结果绑定到一个值。运行此代码看看会发生什么

$ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s Running `target/debug/branches` The value of number is: 5

请记住,代码块会评估为其中的最后一个表达式,而数字本身也是表达式。在这种情况下,整个 if 表达式的值取决于执行哪个代码块。这意味着可能成为 if 的每个分支结果的值必须是相同的类型;在 Listing 3-2 中,if 分支和 else 分支的结果都是 i32 整数。如果类型不匹配,如下例所示,我们将收到错误

文件名:src/main.rs

fn main() { let condition = true; let number = if condition { 5 } else { "six" }; println!("The value of number is: {number}"); }

当我们尝试编译这段代码时,我们会收到一个错误。ifelse 分支具有不兼容的值类型,Rust 指示了在程序中查找问题的确切位置

$ cargo run Compiling branches v0.1.0 (file:///projects/branches) error[E0308]: `if` and `else` have incompatible types --> src/main.rs:4:44 | 4 | let number = if condition { 5 } else { "six" }; | - ^^^^^ expected integer, found `&str` | | | expected because of this For more information about this error, try `rustc --explain E0308`. error: could not compile `branches` (bin "branches") due to 1 previous error

if 代码块中的表达式评估为一个整数,而 else 代码块中的表达式评估为一个字符串。这将不起作用,因为变量必须具有单一类型,并且 Rust 需要在编译时明确知道 number 变量的类型。了解 number 的类型可以让编译器验证在我们使用 number 的任何地方类型都是有效的。如果 number 的类型仅在运行时确定,Rust 将无法做到这一点;如果编译器必须跟踪任何变量的多个假设类型,编译器会变得更加复杂,并且对代码的保证也会减少。

使用循环重复

多次执行一个代码块通常很有用。对于此任务,Rust 提供了几种循环,它们将运行循环体内的代码直到结尾,然后立即从头开始。为了试验循环,让我们创建一个名为 loops 的新项目。

Rust 有三种循环:loopwhilefor。让我们尝试每一种。

使用 loop 重复代码

loop 关键字告诉 Rust 反复执行一个代码块,直到你显式地告诉它停止。

例如,将你的 loops 目录中的 src/main.rs 文件更改为如下所示

文件名:src/main.rs

fn main() { loop { println!("again!"); } }

当我们运行这个程序时,我们将看到 again! 被一遍又一遍地打印出来,直到我们手动停止程序。大多数终端都支持键盘快捷键 ctrl-c 来中断卡在持续循环中的程序。试一试

$ cargo run Compiling loops v0.1.0 (file:///projects/loops) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s Running `target/debug/loops` again! again! again! again! ^Cagain!

符号 ^C 表示你按下了 ctrl-c 的位置。你可能会也可能不会在 ^C 之后看到单词 again! 被打印出来,这取决于代码在收到中断信号时在循环中的位置。

幸运的是,Rust 也提供了一种使用代码跳出循环的方法。你可以在循环内放置 break 关键字,以告诉程序何时停止执行循环。回想一下,我们在猜谜游戏中在“猜对后退出”中做过这件事第 2 章的章节,以便在用户猜对数字赢得游戏时退出程序。

我们还在猜谜游戏中使用过 continue,它在循环中告诉程序跳过循环的本次迭代中任何剩余的代码,并转到下一次迭代。

从循环返回值

loop 的用途之一是重试你明知可能失败的操作,例如检查线程是否已完成其工作。你可能还需要将该操作的结果从循环中传递到代码的其余部分。为此,你可以将你要返回的值添加到用于停止循环的 break 表达式之后;该值将从循环中返回,以便你可以使用它,如下所示

fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }

在循环之前,我们声明一个名为 counter 的变量并将其初始化为 0。然后我们声明一个名为 result 的变量来保存从循环返回的值。在循环的每次迭代中,我们将 1 添加到 counter 变量,然后检查 counter 是否等于 10。当它等于 10 时,我们使用带有值 counter * 2break 关键字。在循环之后,我们使用分号结束将值赋给 result 的语句。最后,我们打印 result 中的值,在本例中为 20

你也可以从循环内部 return。虽然 break 仅退出当前循环,但 return 始终退出当前函数。

循环标签以消除多个循环之间的歧义

如果你有循环嵌套循环,breakcontinue 应用于该点最内层的循环。你可以选择在循环上指定一个循环标签,然后你可以将其与 breakcontinue 一起使用,以指定这些关键字应用于标记的循环而不是最内层的循环。循环标签必须以单引号开头。这是一个带有两个嵌套循环的示例

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; 语句将退出外部循环。这段代码打印

$ cargo run Compiling loops v0.1.0 (file:///projects/loops) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s Running `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,停止循环。可以使用 loopifelsebreak 的组合来实现这样的行为;如果你愿意,你现在可以在程序中尝试一下。但是,这种模式非常常见,以至于 Rust 为其内置了一种语言结构,称为 while 循环。在 Listing 3-3 中,我们使用 while 循环程序三次,每次都倒数,然后在循环之后,打印一条消息并退出。

文件名:src/main.rs
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
Listing 3-3:使用 while 循环在条件保持为 true 时运行代码

这种结构消除了如果你使用 loopifelsebreak 所必需的大量嵌套,并且它更清晰。当条件评估为 true 时,代码运行;否则,它退出循环。

使用 for 循环遍历集合

你还可以使用 while 结构来循环遍历集合的元素,例如数组。例如,Listing 3-4 中的循环打印数组 a 中的每个元素。

文件名:src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } }
Listing 3-4:使用 while 循环遍历集合中的每个元素

在这里,代码通过数组中的元素向上计数。它从索引 0 开始,然后循环直到到达数组中的最后一个索引(即,当 index < 5 不再为 true 时)。运行这段代码将打印数组中的每个元素

$ cargo run Compiling loops v0.1.0 (file:///projects/loops) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s Running `target/debug/loops` the value is: 10 the value is: 20 the value is: 30 the value is: 40 the value is: 50

所有五个数组值都如期出现在终端中。即使 index 在某个时候会达到值 5,循环也会在尝试从数组中获取第六个值之前停止执行。

但是,这种方法容易出错;如果索引值或测试条件不正确,我们可能会导致程序 panic。例如,如果你将 a 数组的定义更改为包含四个元素,但忘记将条件更新为 while index < 4,则代码将 panic。它也很慢,因为编译器会添加运行时代码来执行条件检查,以检查索引是否在每次循环迭代时都在数组的边界内。

作为更简洁的替代方案,你可以使用 for 循环并为集合中的每个项目执行一些代码。for 循环看起来像 Listing 3-5 中的代码。

文件名:src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
Listing 3-5:使用 for 循环遍历集合中的每个元素

当我们运行这段代码时,我们将看到与 Listing 3-4 相同的输出。更重要的是,我们现在提高了代码的安全性,并消除了因超出数组末尾或未走得足够远而错过某些项目而可能导致的错误机会。

使用 for 循环,如果你更改了数组中值的数量,你无需记住更改任何其他代码,就像你在 Listing 3-4 中使用的方法一样。

for 循环的安全性和简洁性使其成为 Rust 中最常用的循环结构。即使在你想运行代码一定次数的情况下,就像 Listing 3-3 中使用 while 循环的倒计时示例一样,大多数 Rustaceans 也会使用 for 循环。实现这一点的方法是使用标准库提供的 Range,它生成从一个数字开始到另一个数字结束之前的所有顺序数字。

这是使用 for 循环和我们尚未讨论的另一种方法 rev 来反转范围的倒计时会是什么样子

文件名:src/main.rs

fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }

这段代码更简洁一些,不是吗?

总结

你成功了!这是一个相当大的章节:你学习了变量、标量和复合数据类型、函数、注释、if 表达式和循环!为了练习本章中讨论的概念,尝试构建程序来执行以下操作

  • 在华氏温度和摄氏温度之间转换温度。
  • 生成第 n 个斐波那契数。
  • 打印圣诞颂歌“圣诞节的十二天”的歌词,利用歌曲中的重复部分。

当你准备好继续前进时,我们将讨论 Rust 中常存在于其他编程语言中的一个概念:所有权。