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,就将要执行的代码块放在条件后面的花括号中。与第 2 章“比较猜测与秘密数字”一节讨论过的 match 表达式分支类似,if 表达式中与条件关联的代码块有时也称为分支。

我们也可以选择添加 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 提供布尔条件。例如,如果我们只希望数字不等于 0 时才运行 if 代码块,可以将 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 表达式中组合 if 和 else,从而处理多个条件。例如:

文件名: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 所示。

文件名:src/main.rs
fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}
代码清单 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}");
}

尝试编译这段代码时会报错。if 和 else 分支的值类型不兼容,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 的类型只能在运行时确定,Rust 就无法进行这种验证;如果编译器必须跟踪每个变量的多种可能类型,编译器会更复杂,对代码所能提供的保证也会更少。

使用循环重复执行

重复执行代码块通常很有用。为此,Rust 提供了几种循环:执行完循环体中的代码后,会立即从头开始。为了试验循环,让我们创建一个名为 loops 的新项目。

Rust 有三种循环:loop、while 和 for。让我们逐个尝试。

使用 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,然后检查它是否等于 10。如果相等,就使用 break 关键字返回值 counter * 2。循环之后,我们用分号结束将该值赋给 result 的语句。最后打印 result 的值,本例中它是 20。

你也可以在循环内部使用 return。break 只会退出当前循环,而 return 总是退出当前函数。

使用循环标签消除歧义

如果循环中嵌套了循环,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; 语句则会退出外层循环。这段代码会打印:

$ 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 来停止循环。你可以组合使用 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!("LIFTOFF!!!");
}
代码清单 3-3:当条件计算结果为 true 时使用 while 循环运行代码

这种结构比组合使用 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!("the value is: {}", a[index]);

        index += 1;
    }
}
代码清单 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 所示。

文件名:src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}
代码清单 3-5:使用 for 循环遍历集合中的每个元素

运行这段代码会得到与代码清单 3-4 相同的输出。更重要的是,这种写法提高了代码的安全性,避免了索引超出数组末尾或遍历不完整而漏掉元素所导致的 bug。for 循环生成的机器码也可能更高效,因为每轮迭代都不需要将索引与数组长度进行比较。

使用 for 循环时,即使更改数组中的值的数量,也不必像代码清单 3-4 的方法那样记得修改其他代码。

for 循环既安全又简洁,因此是 Rust 中最常用的循环结构。即使是像代码清单 3-3 的倒计时示例那样,希望代码运行固定次数的情形,大多数 Rustacean 也会使用 for 循环。可以使用标准库提供的 Range,它会生成从一个数字开始、到另一个数字之前结束的连续数字序列。

下面使用 for 循环和我们尚未讨论过的 rev 方法反转范围,来实现倒计时:

文件名:src/main.rs

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

这段代码看起来更简洁一些,对吧?

总结

你完成了!这一章内容不少:我们学习了变量、标量和复合数据类型、函数、注释、if 表达式和循环!为了练习本章讨论的概念,可以尝试编写程序完成以下任务:

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

准备好继续学习时,我们将讨论 Rust 中一个不常见于其他编程语言的概念:所有权。