Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

控制流

根据条件是否为 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 表达式中的条件相关联的代码块有时被称为分支(arm),就像我们在第 2 章“比较猜测的数字与秘密数字”部分讨论的 match 表达式中的分支一样。

我们还可以选择性地包含一个 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 处理多个条件

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

文件名: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 语句的右侧使用它来将结果赋给一个变量,如示例 3-2 所示。

Filename: 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 的每个分支可能产生的结果值必须是相同的类型;在示例 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 提供了几种循环(loop),它们会执行循环体内的代码直到结尾,然后立即从头开始。为了试验循环,让我们创建一个名为 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 的变量来保存循环返回的值。在循环的每次迭代中,我们给 counter 变量加 1,然后检查 counter 是否等于 10。当等于 10 时,我们使用 break 关键字并带上值 counter * 2。循环之后,我们用分号结束将值赋给 result 的语句。最后,我们打印 result 中的值,本例中为 20

你也可以在循环内部使用 returnbreak 只退出当前循环,而 return 始终退出当前函数。

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

如果存在嵌套循环,breakcontinue 作用于此处最内层的循环。你可以选择在循环上指定一个循环标签(loop label),然后将该标签与 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 循环。在示例 3-3 中,我们使用 while 让程序循环三次,每次倒计数,然后在循环结束后打印一条消息并退出。

Filename: 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 结构来遍历集合的元素,比如数组。例如,示例 3-4 中的循环打印数组 a 中的每个元素。

Filename: 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 循环看起来像示例 3-5 中的代码。

Filename: 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 循环遍历集合中的每个元素

当我们运行这段代码时,会看到与示例 3-4 相同的输出。更重要的是,我们现在提高了代码的安全性,消除了因超出数组末尾或遍历不够远而遗漏某些元素所可能导致的 bug。for 循环生成的机器码也可能更高效,因为不需要在每次迭代时将索引与数组长度进行比较。

使用 for 循环,如果你改变了数组中值的数量,不需要像示例 3-4 中的方法那样记得修改其他代码。

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

下面是使用 for 循环和我们尚未介绍的 rev 方法来反转范围后的倒计时效果:

文件名:src/main.rs

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

这段代码是不是更好看了?

总结

你做到了!这是相当长的一章:你学习了变量、标量和复合数据类型、函数、注释、if 表达式和循环!为了练习本章讨论的概念,尝试编写程序来完成以下任务:

  • 在华氏温度和摄氏温度之间进行转换。
  • 生成第 n 个斐波那契数。
  • 打印圣诞颂歌“The Twelve Days of Christmas“的歌词,利用歌曲中的重复部分。

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