跳到主要内容

控制流语句

判断语句

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。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。if 表达式中与条件关联的代码块有时被叫做 arms,就像第二章 “比较猜测的数字和秘密数字” 部分中讨论到的 match 表达式中的分支一样。

也可以包含一个可选的 else 表达式来提供一个在条件为假时应当执行的代码块,这里我们就这么做了。如果不提供 else 表达式并且条件为假时,程序会直接忽略 if 代码块并继续执行下面的代码。

尝试运行代码,应该能看到如下输出:

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

尝试改变 number 的值使条件为 false 时看看会发生什么:

let number = 7;

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

$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
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 抛出了一个错误:

error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected bool, found integer
|
= note: expected type `bool`
found type `{integer}`

这个错误表明 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](https://rust.bootcss.com/ch03-05-control-flow.html#%E4%BD%BF%E7%94%A8-else-if-%E5%A4%84%E7%90%86%E5%A4%9A%E9%87%8D%E6%9D%A1%E4%BB%B6) 处理多重条件

可以将 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.31 secs
Running `target/debug/branches`
number is divisible by 3

当执行这个程序时,它按顺序检查每个 if 表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会输出 number is divisible by 2,更不会输出 else 块中的 number is not divisible by 4, 3, or 2。原因是 Rust 只会执行第一个条件为真的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。

使用过多的 else if 表达式会使代码显得杂乱无章,所以如果有多于一个 else if 表达式,最好重构代码。为此,第六章会介绍一个强大的 Rust 分支结构(branching construct),叫做 match

[let](https://rust.bootcss.com/ch03-05-control-flow.html#%E5%9C%A8-let-%E8%AF%AD%E5%8F%A5%E4%B8%AD%E4%BD%BF%E7%94%A8-if) 语句中使用 [if](https://rust.bootcss.com/ch03-05-control-flow.html#%E5%9C%A8-let-%E8%AF%AD%E5%8F%A5%E4%B8%AD%E4%BD%BF%E7%94%A8-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.30 secs
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 也准确地指出在程序中的何处发现的这个问题:

error[E0308]: if and else have incompatible types
--> src/main.rs:4:18
|
4 | let number = if condition {
| __________________^
5 | | 5
6 | | } else {
7 | | "six"
8 | | };
| |_____^ expected integer, found &str
|
= note: expected type `{integer}`
found type `&str`

if 代码块中的表达式返回一个整数,而 else 代码块中的表达式返回一个字符串。这不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道 number 变量的类型,这样它就可以在编译时验证在每处使用的 number 变量的类型是有效的。Rust 并不能够在 number 的类型只能在运行时确定的情况下工作;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。

if let 语法让我们以一种不那么冗长的方式结合 iflet,来处理只匹配一个模式的值而忽略其他模式的情况。考虑示例 6-6 中的程序,它匹配一个 Option<u8> 值并只希望当值为 3 时执行代码:


let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}

示例 6-6:match 只关心当值为 Some(3) 时执行代码

我们想要对 Some(3) 匹配进行操作但是不想处理任何其他 Some<u8> 值或 None 值。为了满足 match 表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 _ => (),这样也要增加很多样板代码。

不过我们可以使用 if let 这种更短的方式编写。如下代码与示例 6-6 中的 match 行为一致:


if let Some(3) = some_u8_value {
println!("three");
}

if let 获取通过等号分隔的一个模式和一个表达式。它的工作方式与 match 相同,这里的表达式对应 match 而模式则对应第一个分支。

使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match 强制要求的穷尽性检查。matchif let 之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

换句话说,可以认为 if letmatch 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。

可以在 if let 中包含一个 elseelse 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if letelse。回忆一下示例 6-4 中 Coin 枚举的定义,其 Quarter 成员也包含一个 UsState 值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个 match 表达式:


let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}

或者可以使用这样的 if letelse 表达式:


let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

如果你的程序遇到一个使用 match 表达起来过于啰嗦的逻辑,记住 if let 也在你的 Rust 工具箱中。

总结

现在我们涉及到了如何使用枚举来创建有一系列可列举值的自定义类型。我们也展示了标准库的 Option<T> 类型是如何帮助你利用类型系统来避免出错的。当枚举值包含数据时,你可以根据需要处理多少情况来选择使用 matchif let 来获取并使用这些值。

你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。

为了向你的用户提供一个组织良好的 API,它使用起来很直观并且只向用户暴露他们确实需要的部分,那么现在就让我们转向 Rust 的模块系统吧。

match

Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及它们的作用。match 的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。

可以把 match 表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会通过 match 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。

因为刚刚提到了硬币,让我们用它们来作为一个使用 match 的例子!我们可以编写一个函数来获取一个未知的(美帝)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如示例 6-3 中所示。

fn main() {
let number = 13;
// TODO ^ Try different values for `number`

println!("Tell me about {}", number);
match number {
// Match a single value
1 => println!("One!"),
// Match several values
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
// TODO ^ Try adding 13 to the list of prime values
// Match an inclusive range
13..=19 => println!("A teen"),
// Handle the rest of cases
_ => println!("Ain't special"),
// TODO ^ Try commenting out this catch-all arm
}

let boolean = true;
// Match is an expression too
let binary = match boolean {
// The arms of a match must cover all the possible values
false => 0,
true => 1,
// TODO ^ Try commenting out one of these arms
};

println!("{} -> {}", boolean, binary);
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

示例 6-3:一个枚举和一个以枚举成员作为模式的 match 表达式

拆开 value_in_cents 函数中的 match 来看。首先,我们列出 match 关键字后跟一个表达式,在这个例子中是 coin 的值。这看起来非常像 if 使用的表达式,不过这里有一个非常大的区别:对于 if,表达式必须返回一个布尔值,而这里它可以是任何类型的。例子中的 coin 的类型是示例 6-3 中定义的 Coin 枚举。

接下来是 match 的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值 Coin::Penny 而之后的 => 运算符将模式和将要运行的代码分开。这里的代码就仅仅是值 1。每一个分支之间使用逗号分隔。

match 表达式执行时,它将结果值按顺序与每一个分支的模式相比较。如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。可以拥有任意多的分支:示例 6-3 中的 match 有四个分支。

每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。

如果分支代码较短的话通常不使用大括号,正如示例 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用Coin::Penny 调用时都会打印出 “Lucky penny!”,同时仍然返回代码块最后的值,1


fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let triple = (0, -2, 3);
// TODO ^ Try different values for `triple`

println!("Tell me about {:?}", triple);
// Match can be used to destructure a tuple
match triple {
// Destructure the second and third elements
(0, y, z) => println!("First is `0`, `y` is {:?}, and `z` is {:?}", y, z),
(1, ..) => println!("First is `1` and the rest doesn't matter"),
(.., 2) => println!("last is `2` and the rest doesn't matter"),
(3, .., 4) => println!("First is `3`, last is `4`, and the rest doesn't matter"),
// `..` can be used to ignore the rest of the tuple
_ => println!("It doesn't matter what they are"),
// `_` means don't bind the value to a variable
}
}
fn main() {
// Try changing the values in the array, or make it a slice!
let array = [1, -2, 6];

match array {
// Binds the second and the third elements to the respective variables
[0, second, third] =>
println!("array[0] = 0, array[1] = {}, array[2] = {}", second, third),

// Single values can be ignored with _
[1, _, third] => println!(
"array[0] = 1, array[2] = {} and array[1] was ignored",
third
),

// You can also bind some and ignore the rest
[-1, second, ..] => println!(
"array[0] = -1, array[1] = {} and all the other ones were ignored",
second
),
// The code below would not compile
// [-1, second] => ...

// Or store them in another array/slice (the type depends on
// that of the value that is being matched against)
[3, second, tail @ ..] => println!(
"array[0] = 3, array[1] = {} and the other elements were {:?}",
second, tail
),

// Combining these patterns, we can, for example, bind the first and
// last values, and store the rest of them in a single array
[first, middle @ .., last] => println!(
"array[0] = {}, middle = {:?}, array[2] = {}",
first, middle, last
),
}
}
// `allow` required to silence warnings because only
// one variant is used.
#[allow(dead_code)]
enum Color {
// These 3 are specified solely by their name.
Red,
Blue,
Green,
// These likewise tie `u32` tuples to different names: color models.
RGB(u32, u32, u32),
HSV(u32, u32, u32),
HSL(u32, u32, u32),
CMY(u32, u32, u32),
CMYK(u32, u32, u32, u32),
}

fn main() {
let color = Color::RGB(122, 17, 40);
// TODO ^ Try different variants for `color`

println!("What color is it?");
// An `enum` can be destructured using a `match`.
match color {
Color::Red => println!("The color is Red!"),
Color::Blue => println!("The color is Blue!"),
Color::Green => println!("The color is Green!"),
Color::RGB(r, g, b) =>
println!("Red: {}, green: {}, and blue: {}!", r, g, b),
Color::HSV(h, s, v) =>
println!("Hue: {}, saturation: {}, value: {}!", h, s, v),
Color::HSL(h, s, l) =>
println!("Hue: {}, saturation: {}, lightness: {}!", h, s, l),
Color::CMY(c, m, y) =>
println!("Cyan: {}, magenta: {}, yellow: {}!", c, m, y),
Color::CMYK(c, m, y, k) =>
println!("Cyan: {}, magenta: {}, yellow: {}, key (black): {}!",
c, m, y, k),
// Don't need another arm because all variants have been examined
}
}

对于指针,需要区分解构和取消引用,因为它们是不同的概念,其使用方式与 C/C++ 等语言不同。

取消引用使用 * 解构使用 & 、 ref 和 ref mut

fn main() {
// Assign a reference of type `i32`. The `&` signifies there
// is a reference being assigned.
let reference = &4;

match reference {
// If `reference` is pattern matched against `&val`, it results
// in a comparison like:
// `&i32`
// `&val`
// ^ We see that if the matching `&`s are dropped, then the `i32`
// should be assigned to `val`.
&val => println!("Got a value via destructuring: {:?}", val),
}

// To avoid the `&`, you dereference before matching.
match *reference {
val => println!("Got a value via dereferencing: {:?}", val),
}

// What if you don't start with a reference? `reference` was a `&`
// because the right side was already a reference. This is not
// a reference because the right side is not one.
let _not_a_reference = 3;

// Rust provides `ref` for exactly this purpose. It modifies the
// assignment so that a reference is created for the element; this
// reference is assigned.
let ref _is_a_reference = 3;

// Accordingly, by defining 2 values without references, references
// can be retrieved via `ref` and `ref mut`.
let value = 5;
let mut mut_value = 6;

// Use `ref` keyword to create a reference.
match value {
ref r => println!("Got a reference to a value: {:?}", r),
}

// Use `ref mut` similarly.
match mut_value {
ref mut m => {
// Got a reference. Gotta dereference it before we can
// add anything to it.
*m += 10;
println!("We added 10. `mut_value`: {:?}", m);
},
}
}
fn main() {
let number: u8 = 4;

match number {
i if i == 0 => println!("Zero"),
i if i > 0 => println!("Greater than zero"),
// _ => unreachable!("Should never happen."),
// TODO ^ uncomment to fix compilation
}
}
fn main() {
struct Foo {
x: (u32, u32),
y: u32,
}

// Try changing the values in the struct to see what happens
let foo = Foo { x: (1, 2), y: 3 };

match foo {
Foo { x: (1, b), y } => println!("First of x is 1, b = {}, y = {} ", b, y),

// you can destructure structs and rename the variables,
// the order is not important
Foo { y: 2, x: i } => println!("y is 2, i = {:?}", i),

// and you can also ignore some variables:
Foo { y, .. } => println!("y = {}, we don't care about x", y),
// this will give an error: pattern does not mention field `x`
//Foo { y } => println!("y = {}", y),
}

let faa = Foo { x: (1, 2), y: 3 };

// You do not need a match block to destructure structs:
let Foo { x : x0, y: y0 } = faa;
println!("Outside: x0 = {x0:?}, y0 = {y0}");

// Destructuring works with nested structs as well:
struct Bar {
foo: Foo,
}

let bar = Bar { foo: faa };
let Bar { foo: Foo { x: nested_x, y: nested_y } } = bar;
println!("Nested: nested_x = {nested_x:?}, nested_y = {nested_y:?}");
}

binding

间接访问变量使得无法在不重新绑定的情况下分支和使用该变量。 match 提供用于将值绑定到名称的 @ 印记:

// A function `age` which returns a `u32`.
fn age() -> u32 {
15
}

fn main() {
println!("Tell me what type of person you are");

match age() {
0 => println!("I haven't celebrated my first birthday yet"),
// Could `match` 1 ..= 12 directly but then what age
// would the child be? Instead, bind to `n` for the
// sequence of 1 ..= 12. Now the age can be reported.
n @ 1 ..= 12 => println!("I'm a child of age {:?}", n),
n @ 13 ..= 19 => println!("I'm a teen of age {:?}", n),
// Nothing bound. Return the result.
n => println!("I'm an old person of age {:?}", n),
}
}

您还可以使用绑定来“解构” enum 变体,例如 Option :

fn some_number() -> Option<u32> {
Some(42)
}

fn main() {
match some_number() {
// Got `Some` variant, match if its value, bound to `n`,
// is equal to 42.
Some(n @ 42) => println!("The Answer: {}!", n),
// Match any other number.
Some(n) => println!("Not interesting... {}", n),
// Match anything else (`None` variant).
_ => (),
}
}

Guards

#[allow(dead_code)]
enum Temperature {
Celsius(i32),
Fahrenheit(i32),
}

fn main() {
let temperature = Temperature::Celsius(35);
// ^ TODO try different values for `temperature`

match temperature {
Temperature::Celsius(t) if t > 30 => println!("{}C is above 30 Celsius", t),
// The `if condition` part ^ is a guard
Temperature::Celsius(t) => println!("{}C is equal to or below 30 Celsius", t),

Temperature::Fahrenheit(t) if t > 86 => println!("{}F is above 86 Fahrenheit", t),
Temperature::Fahrenheit(t) => println!("{}F is equal to or below 86 Fahrenheit", t),
}
}

绑定值的模式

匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值的。

作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的 enum,通过改变 Quarter 成员来包含一个 State 值,示例 6-4 中完成了这些修改:


#[derive(Debug)] // 这样可以可以立刻看到州的名称
enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

示例 6-4:Quarter 成员也存放了一个 UsState 值的 Coin 枚举

想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以将其加入收藏。

在这些代码的匹配表达式中,我们在匹配 Coin::Quarter 成员的分支的模式中增加了一个叫做 state 的变量。当匹配到 Coin::Quarter 时,变量 state 将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用 state,如下:


fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}

如果调用 value_in_cents(Coin::Quarter(UsState::Alaska))coin 将是 Coin::Quarter(UsState::Alaska)。当将值与每个分支相比较时,没有分支会匹配,直到遇到 Coin::Quarter(state)。这时,state 绑定的将会是值 UsState::Alaska。接着就可以在 println! 表达式中使用这个绑定了,像这样就可以获取 Coin 枚举的 Quarter 成员中内部的州的值。

匹配 [Option<T>](https://rust.bootcss.com/ch06-02-match.html#%E5%8C%B9%E9%85%8D-optiont)

我们在之前的部分中使用 Option<T> 时,是为了从 Some 中取出其内部的 T 值;我们还可以像处理 Coin 枚举那样使用 match 处理 Option<T>!与其直接比较硬币,我们将比较 Option<T> 的成员,不过 match 表达式的工作方式保持不变。

比如我们想要编写一个函数,它获取一个 Option<i32> 并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回 None 值并不尝试执行任何操作。

得益于 match,编写这个函数非常简单,它将看起来像示例 6-5 中这样:


fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

示例 6-5:一个在 Option<i32> 上使用 match 表达式的函数

匹配 [Some(T)](https://rust.bootcss.com/ch06-02-match.html#%E5%8C%B9%E9%85%8D-somet)

让我们更仔细地检查 plus_one 的第一行操作。当调用 plus_one(five) 时,plus_one 函数体中的 x 将会是值 Some(5)。接着将其与每个分支比较。

None => None,

Some(5) 并不匹配模式 None,所以继续进行下一个分支。

Some(i) => Some(i + 1),

Some(5)Some(i) 匹配吗?当然匹配!它们是相同的成员。i 绑定了 Some 中包含的值,所以 i 的值是 5。接着匹配分支的代码被执行,所以我们将 i 的值加一并返回一个含有值 6 的新 Some

接着考虑下示例 6-5 中 plus_one 的第二个调用,这里 xNone。我们进入 match 并与第一个分支相比较。

None => None,

匹配上了!这里没有值来加一,所以程序结束并返回 => 右侧的值 None,因为第一个分支就匹配到了,其他的分支将不再比较。

match 与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match 一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开始有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直是用户的最爱。

匹配是穷尽的

match 还有另一方面需要讨论。考虑一下 plus_one 函数的这个版本,它有一个 bug 并不能编译:

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}

我们没有处理 None 的情况,所以这些代码会造成一个 bug。幸运的是,这是一个 Rust 知道如何处理的 bug。如果尝试编译这段代码,会得到这个错误:

error[E0004]: non-exhaustive patterns: `None` not covered
-->
|
6 | match x {
| ^ pattern `None` not covered

Rust 知道我们没有覆盖所有可能的情况甚至知道哪些模式被忘记了!Rust 中的匹配是 穷尽的exhaustive(力竭)):必须穷举到最后的可能性来使代码有效。特别的在这个 Option<T> 的例子中,Rust 防止我们忘记明确的处理 None 的情况,这使我们免于假设拥有一个实际上为空的值,这造成了之前提到过的价值亿万的错误。

[_](https://rust.bootcss.com/ch06-02-match.html#_-%E9%80%9A%E9%85%8D%E7%AC%A6) 通配符

Rust 也提供了一个模式用于不想列举出所有可能值的场景。例如,u8 可以拥有 0 到 255 的有效的值,如果我们只关心 1、3、5 和 7 这几个值,就并不想必须列出 0、2、4、6、8、9 一直到 255 的值。所幸我们不必这么做:可以使用特殊的模式 _ 替代:


let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}

_ 模式会匹配所有的值。通过将其放置于其他分支之后,_ 将会匹配所有之前没有指定的可能的值。() 就是 unit 值,所以 _ 的情况什么也不会发生。因此,可以说我们想要对 _ 通配符之前没有列出的所有可能的值不做任何处理。

然而,match 在只关心 一个 情况的场景中可能就有点啰嗦了。为此 Rust 提供了if let

循环语句

while

可以使用 while 结构来遍历集合中的元素,比如数组。例如,看看示例 3-4。

文件名: 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 = index + 1;
}
}

示例 3-4:使用 while 循环遍历集合中的元素

这里,代码对数组中的元素进行计数。它从索引 0 开始,并接着循环直到遇到数组的最后一个索引(这时,index < 5 不再为真)。运行这段代码会打印出数组中的每一个元素:

$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
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。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。

作为更简洁的替代方案,可以使用 for 循环来对一个集合的每个元素执行一些代码。for 循环看起来如示例 3-5 所示:

文件名: src/main.rs

fn main() {
let a = [10, 20, 30, 40, 50];

for element in a.iter() {
println!("the value is: {}", element);
}
}

示例 3-5:使用 for 循环遍历集合中的元素

当运行这段代码时,将看到与示例 3-4 一样的输出。更为重要的是,我们增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug。

例如,在示例 3-4 的代码中,如果从数组 a 中移除一个元素但忘记将条件更新为 while index < 4,代码将会 panic。使用 for 循环的话,就不需要惦记着在改变数组元素个数时修改其他的代码了。

for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-3 中使用 while 循环的倒计时例子,大部分 Rustacean 也会使用 for 循环。这么做的方式是使用 Range,它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。

下面是一个使用 for 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,rev,用来反转 range:

文件名: src/main.rs

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

这段代码看起来更帅气不是吗?

[while](https://rust.bootcss.com/ch03-05-control-flow.html#while-%E6%9D%A1%E4%BB%B6%E5%BE%AA%E7%8E%AF) 条件循环

在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用 break 停止循环。这个循环类型可以通过组合 loopifelsebreak 来实现;如果你喜欢的话,现在就可以在程序中试试。

然而,这个模式太常用了,Rust 为此内置了一个语言结构,它被称为 while 循环。示例 3-3 使用了 while:程序循环三次,每次数字都减一。接着,在循环结束后,打印出另一个信息并退出。

文件名: src/main.rs

fn main() {
let mut number = 3;

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

number = number - 1;
}

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

示例 3-3: 当条件为真时,使用 while 循环运行代码

这种结构消除了很多使用 loopifelsebreak 时所必须的嵌套,这样更加清晰。当条件为真就执行,否则退出循环。

loop

处理嵌套循环时,可以使用 break 或 continue 外部循环。在这些情况下,循环必须用一些 'label 进行注释,并且标签必须传递给 break / continue 语句。

#![allow(unreachable_code, unused_labels)]

fn main() {
'outer: loop {
println!("Entered the outer loop");

'inner: loop {
println!("Entered the inner loop");

// This would break only the inner loop
//break;

// This breaks the outer loop
break 'outer;
}

println!("This point will never be reached");
}

println!("Exited the outer loop");
}

多次执行同一段代码是很常用的,Rust 为此提供了多种 循环loops)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们新建一个叫做 loops 的项目。

Rust 有三种循环:loopwhilefor。我们每一个都试试。

使用 [loop](https://rust.bootcss.com/ch03-05-control-flow.html#%E4%BD%BF%E7%94%A8-loop-%E9%87%8D%E5%A4%8D%E6%89%A7%E8%A1%8C%E4%BB%A3%E7%A0%81) 重复执行代码

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.29 secs
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

符号 ^C 代表你在这按下了ctrl-c。在 ^C 之后你可能看到也可能看不到 again! ,这取决于在接收到终止信号时代码执行到了循环的何处。

幸运的是,Rust 提供了另一种更可靠的退出循环的方式。可以使用 break 关键字来告诉程序何时停止循环。回忆一下在第二章猜猜看游戏的 “猜测正确后退出” 部分使用过它来在用户猜对数字赢得游戏后退出程序。

从循环返回

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。

从loop返回

fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};

assert_eq!(result, 20);
}

for

for in 构造可用于迭代 Iterator 。创建迭代器的最简单方法之一是使用范围符号 a..b 。这会产生从 a (包含)到 b (不包含)的值,步长为 1。

让我们使用 for 而不是 while 来编写 FizzBuzz 。

fn main() {
// `n` will take the values: 1, 2, ..., 100 in each iteration
for n in 1..101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 3 == 0 {
println!("fizz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
}
}
fn main() {
// `n` will take the values: 1, 2, ..., 100 in each iteration
for n in 1..=100 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 3 == 0 {
println!("fizz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
}
}

for和迭代器

for in 构造能够通过多种方式与 Iterator 进行交互。正如有关 Iterator 特征的部分中所讨论的,默认情况下 for 循环会将 into_iter 函数应用于集合。然而,这并不是将集合转换为迭代器的唯一方法。

into_iter 、 iter 和 iter_mut 都通过提供内部数据的不同视图,以不同的方式处理集合到迭代器的转换。

iter - 这会在每次迭代中借用集合中的每个元素。从而使集合保持不变并可在循环后重用。

fn main() {
let names = vec!["Bob", "Frank", "Ferris"];

for name in names.iter() {
match name {
&"Ferris" => println!("There is a rustacean among us!"),
// TODO ^ Try deleting the & and matching just "Ferris"
_ => println!("Hello {}", name),
}
}

println!("names: {:?}", names);
}

into_iter - 这会消耗集合,以便在每次迭代时提供准确的数据。一旦集合被消耗,它就不再可供重用,因为它已在循环内“移动”。

fn main() {
let names = vec!["Bob", "Frank", "Ferris"];

for name in names.into_iter() {
match name {
"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}

println!("names: {:?}", names);
// FIXME ^ Comment out this line
}

iter_mut - 这会可变地借用集合中的每个元素,从而允许就地修改集合。

fn main() {
let mut names = vec!["Bob", "Frank", "Ferris"];

for name in names.iter_mut() {
*name = match name {
&mut "Ferris" => "There is a rustacean among us!",
_ => "Hello",
}
}

println!("names: {:?}", names);
}

在上面的代码片段中,请注意 match 分支的类型,这是迭代类型的关键区别。类型的差异当然意味着能够执行的不同动作。

while let

与 if let 类似, while let 可以使尴尬的 match 序列变得更容易忍受。考虑以下递增 i 的序列:

// Make `optional` of type `Option<i32>`
let mut optional = Some(0);

// Repeatedly try this test.
loop {
match optional {
// If `optional` destructures, evaluate the block.
Some(i) => {
if i > 9 {
println!("Greater than 9, quit!");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
// ^ Requires 3 indentations!
},
// Quit the loop when the destructure fails:
_ => { break; }
// ^ Why should this be required? There must be a better way!
}
}

使用 while let 使这个序列变得更好:

// Make `optional` of type `Option<i32>`
let mut optional = Some(0);

// This reads: "while `let` destructures `optional` into
// `Some(i)`, evaluate the block (`{}`). Else `break`.
while let Some(i) = optional {
if i > 9 {
println!("Greater than 9, quit!");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
// ^ Less rightward drift and doesn't require
// explicitly handling the failing case.
}
// ^ `if let` had additional optional `else`/`else if`
// clauses. `while let` does not have these.
Loading Comments...