模式语法
在本节中,我们收集模式中所有有效的语法,并讨论何时以及为什么可能想要使用每一种语法。
匹配字面值
正如你在第 6 章中看到的,你可以直接将模式与字面值进行匹配。以下代码给出了一些示例
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
这段代码会打印 one,因为 x 中的值是 1。当你希望你的代码在获得特定的具体值时执行操作时,此语法非常有用。
匹配具名变量
具名变量是不可反驳的模式,可以匹配任何值,我们在本书中多次使用过它们。但是,在 match 表达式中使用具名变量时,会有一个复杂情况。由于 match 启动了一个新的作用域,因此在 match 表达式内部作为模式一部分声明的变量将遮蔽在 match 结构外部具有相同名称的变量,就像所有变量的情况一样。在列表 18-11 中,我们声明了一个名为 x 的变量,其值为 Some(5),以及一个名为 y 的变量,其值为 10。然后,我们在值 x 上创建了一个 match 表达式。查看 match 分支中的模式和末尾的 println!,并尝试在运行此代码或进一步阅读之前弄清楚代码将打印什么。
文件名:src/main.rs
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(y) => println!("Matched, y = {y}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
列表 18-11:一个 match 表达式,其分支引入了一个被遮蔽的变量 y
让我们逐步了解 match 表达式运行时会发生什么。第一个 match 分支中的模式与 x 的已定义值不匹配,因此代码继续执行。
第二个 match 分支引入了一个名为 y 的新变量,它将匹配 Some 值内部的任何值。因为我们在 match 表达式内部的新作用域中,所以这是一个新的 y 变量,而不是我们在开头声明的、值为 10 的 y。这个新的 y 绑定将匹配 Some 内部的任何值,这正是我们在 x 中拥有的。因此,这个新的 y 绑定到 x 中 Some 的内部值。该值为 5,因此该分支的表达式执行并打印 Matched, y = 5。
如果 x 是 None 值而不是 Some(5),则前两个分支中的模式将不匹配,因此该值将匹配到下划线。我们没有在下划线分支的模式中引入 x 变量,因此表达式中的 x 仍然是尚未被遮蔽的外部 x。在这种假设情况下,match 将打印 Default case, x = None。
当 match 表达式完成时,它的作用域结束,内部 y 的作用域也随之结束。最后的 println! 产生 at the end: x = Some(5), y = 10。
要创建一个 match 表达式来比较外部 x 和 y 的值,而不是引入一个被遮蔽的变量,我们需要使用 match 守卫条件。我们将在 “带有 Match 守卫的额外条件” 中稍后讨论 match 守卫部分。
多个模式
在 match 表达式中,你可以使用 | 语法匹配多个模式,这是模式或运算符。例如,在以下代码中,我们将 x 的值与 match 分支进行匹配,第一个分支具有或选项,这意味着如果 x 的值与该分支中的任何一个值匹配,则该分支的代码将运行
fn main() { let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } }
这段代码打印 one or two。
使用 ..= 匹配值的范围
..= 语法允许我们匹配包含范围内的值。在以下代码中,当模式匹配给定范围内的任何值时,该分支将执行
fn main() { let x = 5; match x { 1..=5 => println!("one through five"), _ => println!("something else"), } }
如果 x 是 1、2、3、4 或 5,则第一个分支将匹配。对于多个匹配值,此语法比使用 | 运算符表达相同的想法更方便;如果我们要使用 |,我们将不得不指定 1 | 2 | 3 | 4 | 5。指定范围要短得多,特别是当我们想要匹配例如 1 到 1,000 之间的任何数字时!
编译器在编译时检查范围是否为空,并且由于 Rust 能够判断范围是否为空的类型只有 char 和数值类型,因此范围仅允许用于数值或 char 值。
这是一个使用 char 值范围的示例
fn main() { let x = 'c'; match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } }
Rust 可以判断 'c' 在第一个模式的范围内,并打印 early ASCII letter。
解构以分解值
我们还可以使用模式来解构结构体、枚举和元组,以使用这些值的不同部分。让我们逐步了解每个值。
解构结构体
列表 18-12 显示了一个具有两个字段 x 和 y 的 Point 结构体,我们可以使用带有 let 语句的模式将其分解。
文件名:src/main.rs
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; assert_eq!(0, a); assert_eq!(7, b); }
列表 18-12:将结构体的字段解构为单独的变量
此代码创建了变量 a 和 b,它们匹配 p 结构体的 x 和 y 字段的值。此示例表明模式中变量的名称不必与结构体的字段名称匹配。但是,将变量名称与字段名称匹配是很常见的,这样可以更容易记住哪些变量来自哪些字段。由于这种常见的用法,并且由于编写 let Point { x: x, y: y } = p; 包含大量重复,因此 Rust 为匹配结构体字段的模式提供了一个简写形式:你只需要列出结构体字段的名称,从模式创建的变量将具有相同的名称。列表 18-13 的行为方式与列表 18-12 中的代码相同,但 let 模式中创建的变量是 x 和 y 而不是 a 和 b。
文件名:src/main.rs
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x, y } = p; assert_eq!(0, x); assert_eq!(7, y); }
列表 18-13:使用结构体字段简写解构结构体字段
此代码创建了变量 x 和 y,它们匹配 p 变量的 x 和 y 字段。结果是变量 x 和 y 包含来自 p 结构体的值。
我们还可以在结构体模式中解构字面值作为一部分,而不是为所有字段创建变量。这样做允许我们测试某些字段的特定值,同时创建变量来解构其他字段。
在列表 18-14 中,我们有一个 match 表达式,它将 Point 值分为三种情况:直接位于 x 轴上的点(当 y = 0 时为真)、位于 y 轴上的点(x = 0)或两者都不是。
文件名:src/main.rs
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("On the x axis at {x}"), Point { x: 0, y } => println!("On the y axis at {y}"), Point { x, y } => { println!("On neither axis: ({x}, {y})"); } } }
列表 18-14:在一个模式中解构和匹配字面值
第一个分支将匹配任何位于 x 轴上的点,方法是指定如果 y 字段的值与字面值 0 匹配,则匹配。该模式仍然创建一个 x 变量,我们可以在此分支的代码中使用它。
类似地,第二个分支匹配 y 轴上的任何点,方法是指定如果 x 字段的值为 0,则匹配,并为 y 字段的值创建一个变量 y。第三个分支不指定任何字面值,因此它匹配任何其他 Point,并为 x 和 y 字段创建变量。
在此示例中,值 p 通过 x 包含 0 来匹配第二个分支,因此这段代码将打印 On the y axis at 7。
请记住,match 表达式一旦找到第一个匹配模式就会停止检查分支,因此即使 Point { x: 0, y: 0} 在 x 轴和 y 轴上,此代码也只会打印 On the x axis at 0。
解构枚举
我们已经在本书中解构了枚举(例如,第 6 章中的列表 6-5),但尚未明确讨论解构枚举的模式与枚举中存储的数据的定义方式相对应。例如,在列表 18-15 中,我们使用了列表 6-2 中的 Message 枚举,并编写了一个 match,其模式将解构每个内部值。
文件名:src/main.rs
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => { println!("The Quit variant has no data to destructure."); } Message::Move { x, y } => { println!("Move in the x direction {x} and in the y direction {y}"); } Message::Write(text) => { println!("Text message: {text}"); } Message::ChangeColor(r, g, b) => { println!("Change the color to red {r}, green {g}, and blue {b}") } } }
列表 18-15:解构保存不同类型值的枚举变体
这段代码将打印 Change the color to red 0, green 160, and blue 255。尝试更改 msg 的值以查看其他分支的代码运行。
对于没有任何数据的枚举变体,例如 Message::Quit,我们无法进一步解构该值。我们只能匹配字面值 Message::Quit 值,并且该模式中没有变量。
对于类似结构体的枚举变体,例如 Message::Move,我们可以使用类似于我们指定匹配结构体的模式。在变体名称之后,我们放置花括号,然后列出带有变量的字段,以便我们分解这些部分以在此分支的代码中使用。这里我们使用了列表 18-13 中的简写形式。
对于类似元组的枚举变体,例如保存一个元素元组的 Message::Write 和保存三个元素元组的 Message::ChangeColor,该模式类似于我们指定匹配元组的模式。模式中变量的数量必须与我们要匹配的变体中元素的数量匹配。
解构嵌套结构体和枚举
到目前为止,我们的示例都是匹配一个深度级别的结构体或枚举,但是匹配也可以在嵌套项上工作!例如,我们可以重构列表 18-15 中的代码以支持 ChangeColor 消息中的 RGB 和 HSV 颜色,如列表 18-16 所示。
enum Color { Rgb(i32, i32, i32), Hsv(i32, i32, i32), } enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(Color), } fn main() { let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); match msg { Message::ChangeColor(Color::Rgb(r, g, b)) => { println!("Change color to red {r}, green {g}, and blue {b}"); } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!("Change color to hue {h}, saturation {s}, value {v}") } _ => (), } }
列表 18-16:匹配嵌套枚举
match 表达式中第一个分支的模式匹配包含 Color::Rgb 变体的 Message::ChangeColor 枚举变体;然后该模式绑定到三个内部 i32 值。第二个分支的模式也匹配 Message::ChangeColor 枚举变体,但内部枚举匹配 Color::Hsv。我们可以在一个 match 表达式中指定这些复杂的条件,即使涉及两个枚举。
解构结构体和元组
我们可以以更复杂的方式混合、匹配和嵌套解构模式。以下示例显示了一个复杂的解构,其中我们将结构体和元组嵌套在元组内部,并解构所有基本值
fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); }
这段代码让我们将复杂类型分解为其组成部分,以便我们可以单独使用我们感兴趣的值。
使用模式进行解构是一种方便的方式,可以单独使用值的片段,例如结构体中每个字段的值。
忽略模式中的值
你已经看到,有时忽略模式中的值很有用,例如在 match 的最后一个分支中,获得一个实际上不执行任何操作但确实考虑了所有剩余可能值的通配符。有几种方法可以忽略模式中的整个值或部分值:使用 _ 模式(你已经见过)、在另一个模式中使用 _ 模式、使用以下划线开头的名称或使用 .. 忽略值的剩余部分。让我们探讨如何以及为什么使用这些模式中的每一种。
使用 _ 忽略整个值
我们已经将下划线用作通配符模式,它将匹配任何值,但不绑定到该值。这在 match 表达式的最后一个分支中特别有用,但我们也可以在任何模式中使用它,包括函数参数,如列表 18-17 所示。
文件名:src/main.rs
fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {y}"); } fn main() { foo(3, 4); }
列表 18-17:在函数签名中使用 _
这段代码将完全忽略作为第一个参数传递的值 3,并将打印 This code only uses the y parameter: 4。
在大多数情况下,当你不再需要特定的函数参数时,你会更改签名,使其不包含未使用的参数。忽略函数参数在某些情况下可能特别有用,例如,当你实现一个 trait 时,你需要某种类型签名,但你的实现中的函数体不需要其中一个参数。然后,你可以避免收到关于未使用函数参数的编译器警告,就像你使用名称时一样。
使用嵌套的 _ 忽略值的部分
我们还可以在另一个模式内部使用 _ 来仅忽略值的一部分,例如,当我们只想测试值的一部分,但对于我们想要运行的相应代码中的其他部分没有用处时。列表 18-18 显示了负责管理设置值的代码。业务需求是,不允许用户覆盖设置的现有自定义项,但可以取消设置该设置,并在当前未设置的情况下为其赋值。
fn main() { let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); } _ => { setting_value = new_setting_value; } } println!("setting is {setting_value:?}"); }
列表 18-18:当我们不需要使用 Some 变体内部的值时,在匹配 Some 变体的模式中使用下划线
这段代码将打印 Can't overwrite an existing customized value,然后打印 setting is Some(5)。在第一个 match 分支中,我们不需要匹配或使用任何一个 Some 变体内部的值,但我们确实需要测试 setting_value 和 new_setting_value 何时为 Some 变体的情况。在这种情况下,我们打印不更改 setting_value 的原因,并且它不会被更改。
在所有其他情况下(如果 setting_value 或 new_setting_value 为 None),由第二个分支中的 _ 模式表示,我们希望允许 new_setting_value 变为 setting_value。
我们还可以在一个模式中的多个位置使用下划线来忽略特定值。列表 18-19 显示了一个忽略包含五个项的元组中的第二个和第四个值的示例。
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {first}, {third}, {fifth}") } } }
列表 18-19:忽略元组的多个部分
这段代码将打印 Some numbers: 2, 8, 32,值 4 和 16 将被忽略。
通过以下划线开头来忽略未使用的变量
如果你创建了一个变量但没有在任何地方使用它,Rust 通常会发出警告,因为未使用的变量可能是一个错误。但是,有时能够创建一个你尚未使用但将来会使用的变量很有用,例如当你进行原型设计或刚开始一个项目时。在这种情况下,你可以通过以下划线开头变量的名称来告诉 Rust 不要警告你关于未使用的变量。在列表 18-20 中,我们创建了两个未使用的变量,但是当我们编译这段代码时,我们应该只会收到关于其中一个变量的警告。
文件名:src/main.rs
fn main() { let _x = 5; let y = 10; }
列表 18-20:以下划线开头变量名称以避免收到未使用的变量警告
在这里,我们收到了关于未使用变量 y 的警告,但是我们没有收到关于未使用 _x 的警告。
请注意,仅使用 _ 和使用以下划线开头的名称之间存在细微的差别。语法 _x 仍然将值绑定到变量,而 _ 根本不绑定。为了展示这种区别很重要的情况,列表 18-21 将为我们提供一个错误。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
列表 18-21:以下划线开头的未使用变量仍然绑定该值,这可能会取得该值的所有权
我们将收到一个错误,因为 s 值仍将被移动到 _s 中,这阻止了我们再次使用 s。但是,单独使用下划线永远不会绑定到该值。列表 18-22 将在没有任何错误的情况下编译,因为 s 没有移动到 _ 中。
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{s:?}"); }
列表 18-22:使用下划线不会绑定值
这段代码可以正常工作,因为我们从未将 s 绑定到任何东西;它没有被移动。
使用 .. 忽略值的剩余部分
对于具有多个部分的值,我们可以使用 .. 语法来使用特定部分并忽略其余部分,从而避免为每个忽略的值列出下划线。.. 模式忽略我们在模式的其余部分中未显式匹配的值的任何部分。在列表 18-23 中,我们有一个 Point 结构体,它保存了三维空间中的坐标。在 match 表达式中,我们只想对 x 坐标进行操作,并忽略 y 和 z 字段中的值。
fn main() { struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x is {x}"), } }
列表 18-23:通过使用 .. 忽略 Point 的除 x 之外的所有字段
我们列出了 x 值,然后只包含 .. 模式。这比必须列出 y: _ 和 z: _ 更快,特别是当我们在处理具有大量字段的结构体时,而只有一两个字段是相关的。
语法 .. 将扩展到所需的尽可能多的值。列表 18-24 显示了如何将 .. 与元组一起使用。
文件名:src/main.rs
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } }
列表 18-24:仅匹配元组中的第一个和最后一个值,并忽略所有其他值
在这段代码中,第一个和最后一个值与 first 和 last 匹配。.. 将匹配并忽略中间的所有内容。
但是,使用 .. 必须是明确的。如果不清楚哪些值旨在用于匹配,哪些应该被忽略,Rust 将给我们一个错误。列表 18-25 显示了一个不明确地使用 .. 的示例,因此它不会编译。
文件名:src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
列表 18-25:尝试以不明确的方式使用 ..
当我们编译这个例子时,我们得到了这个错误
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Rust 不可能确定在与 second 匹配值之前要忽略元组中的多少个值,然后再忽略多少个值。这段代码可能意味着我们想要忽略 2,将 second 绑定到 4,然后忽略 8、16 和 32;或者我们想要忽略 2 和 4,将 second 绑定到 8,然后忽略 16 和 32;等等。变量名 second 对 Rust 没有任何特殊意义,因此我们得到了一个编译器错误,因为像这样在两个地方使用 .. 是不明确的。
带有 Match 守卫的额外条件
Match 守卫是一个额外的 if 条件,在 match 分支中的模式之后指定,它也必须匹配才能选择该分支。Match 守卫对于表达比仅模式允许的更复杂的想法很有用。
该条件可以使用在模式中创建的变量。列表 18-26 显示了一个 match,其中第一个分支具有模式 Some(x),并且还具有 if x % 2 == 0 的 match 守卫(如果该数字为偶数,则为真)。
fn main() { let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {x} is even"), Some(x) => println!("The number {x} is odd"), None => (), } }
列表 18-26:向模式添加 match 守卫
此示例将打印 The number 4 is even。当将 num 与第一个分支中的模式进行比较时,它匹配,因为 Some(4) 匹配 Some(x)。然后,match 守卫检查 x 除以 2 的余数是否等于 0,并且由于它等于 0,因此选择了第一个分支。
如果 num 是 Some(5),则第一个分支中的 match 守卫将为假,因为 5 除以 2 的余数为 1,不等于 0。然后 Rust 将转到第二个分支,该分支将匹配,因为第二个分支没有 match 守卫,因此匹配任何 Some 变体。
没有办法在模式中表达 if x % 2 == 0 条件,因此 match 守卫使我们能够表达这种逻辑。这种额外表达能力的缺点是,带有 match 守卫的分支不“计入”穷尽性。因此,即使我们添加了 Some(x) if x % 2 == 1 作为附加分支,我们仍然需要未受保护的 Some(x) 分支。
在列表 18-11 中,我们提到可以使用 match 守卫来解决我们的模式遮蔽问题。回想一下,我们在 match 表达式中的模式内部创建了一个新变量,而不是使用 match 外部的变量。这个新变量意味着我们无法针对外部变量的值进行测试。列表 18-27 显示了我们如何使用 match 守卫来解决此问题。
文件名:src/main.rs
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(n) if n == y => println!("Matched, n = {n}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
列表 18-27:使用 match 守卫来测试与外部变量的相等性
这段代码现在将打印 Default case, x = Some(5)。第二个 match 分支中的模式没有引入会遮蔽外部 y 的新变量 y,这意味着我们可以在 match 守卫中使用外部 y。我们没有将模式指定为 Some(y)(这将遮蔽外部 y),而是指定了 Some(n)。这创建了一个新变量 n,它不会遮蔽任何东西,因为 match 外部没有 n 变量。
match 守卫 if n == y 不是模式,因此不引入新变量。此 y 是外部 y 而不是新的被遮蔽的 y,我们可以通过将 n 与 y 进行比较来查找具有与外部 y 相同的值的值。
你还可以在 match 守卫中使用 or 运算符 | 来指定多个模式;match 守卫条件将应用于所有模式。列表 18-28 显示了将使用 | 的模式与 match 守卫组合时的优先级。此示例的重要部分是 if y match 守卫应用于 4、5 和 6,即使它看起来好像 if y 仅应用于 6。
fn main() { let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } }
列表 18-28:将多个模式与 match 守卫组合
match 条件声明,只有当 x 的值等于 4、5 或 6 且 y 为 true 时,该分支才匹配。当这段代码运行时,第一个分支的模式匹配,因为 x 是 4,但是 match 守卫 if y 为假,因此未选择第一个分支。代码继续执行到第二个分支,该分支确实匹配,并且此程序打印 no。原因是 if 条件应用于整个模式 4 | 5 | 6,而不仅仅是最后一个值 6。换句话说,match 守卫相对于模式的优先级行为类似于这样
(4 | 5 | 6) if y => ...
而不是这样
4 | 5 | (6 if y) => ...
运行代码后,优先级行为很明显:如果 match 守卫仅应用于使用 | 运算符指定的值列表中的最后一个值,则该分支将匹配,并且程序将打印 yes。
@ 绑定
at 运算符 @ 允许我们创建一个变量,该变量在测试值是否与模式匹配的同时保存该值。在列表 18-29 中,我们想要测试 Message::Hello id 字段是否在范围 3..=7 内。我们还希望将该值绑定到变量 id_variable,以便我们可以在与该分支关联的代码中使用它。我们可以将此变量命名为 id,与字段相同,但在此示例中,我们将使用不同的名称。
fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7, } => println!("Found an id in range: {id_variable}"), Message::Hello { id: 10..=12 } => { println!("Found an id in another range") } Message::Hello { id } => println!("Found some other id: {id}"), } }
列表 18-29:使用 @ 在模式中绑定到值,同时对其进行测试
此示例将打印 Found an id in range: 5。通过在范围 3..=7 之前指定 id_variable @,我们在捕获匹配范围的任何值的同时,也在测试该值是否与范围模式匹配。
在第二个分支中,我们只在模式中指定了一个范围,与该分支关联的代码没有包含 id 字段实际值的变量。id 字段的值可能是 10、11 或 12,但与该模式相关的代码不知道是哪个值。模式代码无法使用 id 字段中的值,因为我们没有将 id 值保存在变量中。
在最后一个分支中,虽然我们指定了一个没有范围的变量,但我们仍然可以在该分支的代码中使用名为 id 的变量来访问该值。 这是因为我们使用了结构体字段简写语法。 然而,与前两个分支不同,我们没有对这个分支中 id 字段的值进行任何测试:任何值都会匹配这个模式。
使用 @ 允许我们在一个模式中测试一个值并将其保存在一个变量中。
总结
Rust 的模式在区分不同类型的数据时非常有用。当在 match 表达式中使用时,Rust 确保你的模式覆盖所有可能的值,否则你的程序将无法编译。 let 语句和函数参数中的模式使这些构造更实用,能够在赋值给变量的同时将值解构为更小的部分。 我们可以创建简单或复杂的模式来满足我们的需求。
接下来,在本书的倒数第二章中,我们将了解 Rust 各种功能的一些高级方面。