控制流

根据条件是否为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 表达式中的条件关联的代码块有时被称为 , 就像我们在第 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

Handling Multiple Conditions with 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 表达式会使代码变得杂乱,因此如果你有多个 else if,你可能需要重构代码。第 6 章描述了一种强大的 Rust 分支结构,称为 match,适用于这些情况。

Using if in a let Statement

因为 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: Assigning the result of an if expression to a variable

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 提供了几个循环,这些循环将运行循环体内的代码直到结束,然后立即从头开始。为了试验循环,让我们创建一个名为loops的新项目。

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

Repeating Code with loop

loop 关键字告诉 Rust 重复执行一段代码,直到你明确要求它停止。

例如,将 src/main.rs 文件更改为您 loops 目录中的内容,使其看起来像这样:

文件名: 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,它在循环中告诉程序跳过本次循环中剩余的代码并进入下一次循环。

Returning Values from Loops

一个 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。当它等于时,我们使用带有值 counter * 2break 关键字。在循环之后,我们使用分号结束将值赋给 result 的语句。最后,我们打印 result 中的值,在这种情况下是 20

你也可以从循环内部 return。虽然 break 只退出当前循环,return 总是退出当前函数。

Loop Labels to Disambiguate Between Multiple Loops

如果您有嵌套的循环,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

Conditional Loops with 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: Using a while loop to run code while a condition holds true

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

Looping Through a Collection with 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: Looping through each element of a collection using a while loop

这里,代码通过数组中的元素进行计数。它从索引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,但在尝试从数组中获取第六个值之前,循环已经停止执行。

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

作为更简洁的替代方案,您可以使用 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: Looping through each element of a collection using a for loop

当我们运行这段代码时,我们将看到与清单 3-4 中相同的输出。更重要的是,我们现在提高了代码的安全性,并消除了因超出数组末尾或未能足够深入而导致某些项被遗漏的错误可能性。

使用 for 循环,如果你改变了数组中的值的数量,你不需要记住去改变任何其他代码,而使用列表 3-4 中的方法则需要。

for 循环的安全性和简洁性使它们成为 Rust 中使用最广泛的循环结构。即使在您希望运行某段代码特定次数的情况下,例如在使用 while 循环的倒计时示例(如列表 3-3 所示),大多数 Rustaceans 也会使用 for 循环。实现这一点的方法是使用标准库提供的 Range,它会生成从一个数字开始到另一个数字之前的所有数字。

这里是一个使用 for 循环和我们尚未讨论的另一种方法 rev 来反转范围的倒计时示例:

文件名: src/main.rs

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

这段代码更好看,不是吗?

摘要

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

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

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