控制流

根据条件是否为 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 [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 [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 [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,用于处理这种情况。

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 [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。让我们尝试一下每一种。

使用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 [unoptimized + debuginfo] target(s) in 0.29s
     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时,我们使用break关键字,并使用counter * 2的值。在循环之后,我们使用分号来结束将值赋值给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 [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循环程序三次,每次倒计时,然后在循环结束后,打印一条消息并退出。

文件名:src/main.rs

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

清单3-3:使用while循环在条件成立时运行代码

这种结构消除了使用loopifelsebreak时所需的许多嵌套,并且更加清晰。当条件评估为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 [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中的代码。

文件名: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中相同的输出。更重要的是,我们现在提高了代码的安全性,消除了超出数组末尾或没有走得足够远而错过某些项目的错误可能性。

使用for循环,如果更改数组中的值数量,您无需像在清单 3-4 中使用的方法那样记住更改任何其他代码。

for循环的安全性与简洁性使其成为 Rust 中最常用的循环结构。即使在您想要运行某些代码一定次数的情况下,例如在清单 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 中一个在其他编程语言中并不常见的概念:所有权。