【WEB3.0零基础转换笔记】Rust编程篇-第4讲:控制流

4.1 Rocket Pool介绍

Learn more about Rocket Pool @ https://rocketpool.net/.

4.2 if else

4.2.1 掌握条件逻辑:Rust 中的基本 if/else 语句

条件语句是编程的基础,它们允许你的代码根据特定标准做出决策并执行不同的路径。在 Rust 中,if/else 结构提供了这种能力,使你能够控制程序的执行流。

4.2.2 核心概念

If/else 语句会评估一个条件。如果条件为真,则执行特定的代码块。如果为假,则可以选择性地检查另一个 else if 条件,或者执行一个可选的 else 块作为默认的回退。

4.2.3 语法

Rust 中 if/else 语句的结构如下:

rust 复制代码
if condition {
    // Code to execute if condition is true
} else if another_condition {
    // Code to execute if another_condition is true
} else {
    // Code to execute if all preceding conditions are false
}

示例: 让我们考虑一个实际例子,即检查一个 u32 的值x

rust 复制代码
// examples/if_else.rs
#![allow(unused)] // Attribute to allow unused code/variables

fn main() {
    let x: u32 = 10; // Declare an unsigned 32-bit integer x with value 10

    if x > 0 {
        println!("x > 0");
    } else if x < 0 {
        // For a u32, x < 0 will always be false.
        println!("x < 0");
    } else {
        println!("x = 0");
    }
}

剖析示例:

(1)我们声明了一个类型为 u32(无符号32位整数)的变量 x,并将其初始化为 10。无符号整数只能持有非负值。

(2)第一个条件 if x > 0(评估 10 > 0)为 true

(3)因此,这个if 代码块内的代码println!("x > 0"); 被执行,并在控制台打印出 "x > 0"。

(4)由于第一个条件已被满足,后续的 else ifelse 代码块 被完全跳过。

关于类型和编译器警告的说明: 在此示例中,xu32 类型。由于 u32 代表无符号整数(0 和正值),条件 x < 0 永远不可能为真。Rust 编译器足够智能,能够识别这一点,并通常会发出警告,例如 "warning: comparison is useless due to type limits"。这表明对于 u32 类型来说,else if x < 0 分支在逻辑上是不可达的。

4.2.4 在 Rust 中利用 if/else 作为表达式

在 Rust 中,if/else 不仅可以控制流程,还能作为表达式返回值,这允许我们将条件判断的结果直接赋值给变量。使用时需注意:每个分支(ifelse ifelse)的最后一个表达式就是该分支的返回值,因此末尾不能加分号 ,否则会变成语句导致类型不匹配;同时,所有分支必须返回相同类型的值,整个 let 语句最后才需要分号结束。这种设计让条件赋值变得简洁而富有表现力。

1、概念:从 if/else 返回值

你可以使用 if/else 块来确定赋值给变量的值。这里的关键规则是 if/else 表达式的 所有分支 (即 if 块、任何 else if 块else 块)必须返回相同类型的值。如果类型不一致,编译器将会报错。

2、 if/else 表达式的语法

当使用 if/else 赋值时,语法如下:

rust 复制代码
let variable_name = if condition {
    value_if_true // Note: No semicolon here
} else if another_condition {
    value_if_another_true // No semicolon here
} else {
    value_if_false // No semicolon here
}; // Semicolon here, for the 'let' statement

当代码块旨在返回某个值时,注意在每个块内的值(如 value_if_true 等)后面没有分号。分号仅出现在整个 let 语句的末尾。

**示例:**有条件地赋值。

让我们修改之前的例子。我们将保留变量 xu32 类型),并使用一个 if/else 表达式,根据 x 的值给一个新变量 zi32 类型------有符号 32 位整数)赋值。

rust 复制代码
// examples/if_else.rs
#![allow(unused)]

fn main() {
    let x: u32 = 10;

    // This is the original if-else from the previous example.
    // It's included here to match the video's progression but can be removed
    // if only the expression-based assignment is needed.
    if x > 0 {
        println!("x > 0 (from basic if-else, first check)");
    } else if x < 0 {
        println!("x < 0 (from basic if-else, first check)");
    } else {
        println!("x = 0 (from basic if-else, first check)");
    }

    // if-else as an expression
    let z: i32 = if x > 0 {
        println!("x > 0 (evaluating for z assignment)"); // This line also executes
        1  // Value returned for z if x > 0
    } else if x < 0 {
        println!("x < 0 (evaluating for z assignment)");
        -1 // Value returned for z if x < 0
    } else {
        println!("x = 0 (evaluating for z assignment)");
        0  // Value returned for z if x is 0
    }; // Semicolon for the `let z` statement

    println!("z = {}", z);
}

理解该表达式:

(1)声明了一个类型为 i32(有符号整数,可以是正数、负数或零)的新变量 z

(2)= 右侧的 if/else 块决定了 z 的值。

(3)由于 x10,条件 x > 0 为真。

(4)第一个代码块被执行:

  • println!("x > 0 (evaluating for z assignment)"); 被执行,打印其消息。

  • 然后,值 1 是该块中的最后一个表达式。因为它没有分号,所以它成为该块产出的值。

(5)这个产出的值 1 被赋值给 z

(6)最后,println!("z = {}+", z); 将输出 "z = 1"

按照视频的演示,控制台输出将先显示第一个基本 if/else 中的 println!,然后显示 if/else 表达式块内的 println!,最后才显示最终的 z 值。

关于分号和返回值的关键要点:

  • 块的隐式返回: 各自块末尾的值 1-10 没有分号。这一点至关重要。在 Rust 中,块中的最后一个表达式如果后面没有分号,那么这个表达式就是该块计算得出的值。如果加了分号(例如 1;),1 就变成了一条语句,该块将隐式返回 ()(单元类型)。这会导致类型不匹配错误,因为 z 期望的是 i32,而不是 ()

  • let 语句 的分号:if/else 表达式用于赋值时,它是 let 语句的一部分。因此,需要在最后的 else 块的结束大括号 } 之后加上一个分号,以结束 let z = ...; 语句。

  • 此处避免使用 return 关键字 如果你的目标是让 if/else 表达式产生一个值用于赋值,那么在这些块内部不要使用 return 关键字(例如 return 1;)。使用 return 1; 会试图从整个 main 函数(或这段代码所在的任何函数)中返回 1,而不仅仅是从 if 块返回到变量 z。如果函数的返回类型不匹配,这很可能会导致类型错误。要让块为表达式产生一个值,只需将该值作为块中的最后一个表达式,并且末尾不加分号即可。

4.2.5 Rust if/else 的关键考虑因素与最佳实践

要在 Rust 中有效且符合习惯地使用 if/else,请牢记这些重要的概念和语法规则:

(1) if/else 是表达式

与其他一些语言中将 if/else 纯粹作为控制流语句不同,在 Rust 中它是一个表达式。这意味着它可以计算出一个值,从而允许像 let result = if condition { val_a } else { val_b }; 这样优雅的赋值方式。

(2)块的隐式返回

任何 Rust 块(由 {} 括起来的代码序列)中的最后一个表达式都会被隐式地作为该块的值返回,前提是它不以分号结尾。这正是 if/else 分支能够产生值的机制。

(3)分支间的类型一致性

当使用 if/else 作为表达式来赋值时,所有可能的分支(if 块、所有 else if 块以及 else 块)必须计算出相同类型的值。如果它们类型不同,Rust 编译器将报告类型不匹配错误。例如,如果你试图将结果赋值给一个变量,就不能让一个分支返回整数而另一个分支返回字符串。

(4)分号位置至关重要

  • 省略分号: 在用于产生值的 if/else 表达式的块末尾,其产生值的表达式后面不要加分号(例如,块内写 1 而不是 1;)。

  • let 语句的末尾加上分号,即使该语句使用了 if/else 表达式进行赋值(例如 let result = if condition { value_a } else { value_b };)。

(5)条件表达式无需括号 Rust 的 if 条件表达式不需要用括号括起来。你应该写成 if x > 0 而不是 if (x > 0)。虽然括号可用于在条件内部对复杂逻辑运算进行分组(例如 if (x > 0 && y < 10) || z == 0),但仅包裹整个条件时并不需要它们。

(6)花括号是强制性的ifelse ifelse 关联的代码块必须始终用花括号 {} 括起来。即使代码块只包含一行代码也是如此。这条规则增强了代码清晰度,并有助于避免在允许可选花括号的语言中常见的错误。

rust 复制代码
// Correct:
if x > 5 {
    println!("x is greater than 5");
}

// Also correct (single line, but braces still required):
if x < 0 { println!("x is negative"); }

// Incorrect (will not compile):
// if x > 5 println!("x is greater than 5");

(7) return 关键字的行为: 需特别留意 return 关键字。如果你在作为赋值一部分(如 let z = if ...)的 if/else 表达式内部使用了 return some_value;,它会导致整个所在函数(例如 main)尝试返回 some_value,而不仅仅是让 some_value 成为 if/else 表达式产生的值。要让 if/else 块为该表达式产生一个值,请确保该值是块中的最后一项,且其末尾不带分号。

4.2 if...else...练习

练习1:返回x和y的最小值。

rust 复制代码
#![allow(unused)]

pub fn min(x: i32, y: i32) -> i32 {
    if x > y {
        return y;
    } else if x < y {
        return x;
    } else {
        return 0;
    };
}

fn main() {
    let x: i32 = 23;
    let y: i32 = 15;
    let min = min(x, y);
    println!("{x}与{y}的最小值为{min}");
}

练习2:返回x和y的最大值。

rust 复制代码
#![allow(unused)]

pub fn max(x: i32, y: i32) -> i32 {
    if x > y {
        return x;
    } else if x < y {
        return y;
    } else {
        return 0;
    };
}

fn main() {
    let x: i32 = 23;
    let y: i32 = 15;
    let max = max(x, y);
    println!("{x}与{y}的最小值为{max}");
}

练习3:返回 x 的符号。如果 x 为负数,返回 -1,否则返回 1。

rust 复制代码
#![allow(unused)]

pub fn sign(x: i32) -> i32 {
    let fuhao: i32 = if x < 0 {
        -1
    } else {
        1
    };
    return fuhao;
}

fn main() {
    let x = -23;
    let sign: i32 = sign(x);
    println!("{sign}");
}

4.3 循环

Rust 提供了三种主要循环:loop 创建一个无限循环,直到显式使用 break 退出,且 break 后可跟表达式将值从循环中返回;while 在条件为 true 时重复执行代码块,适用于条件驱动的重复;for 是最常用的集合遍历方式,可安全迭代数组、范围或任何实现了 IntoIterator 的类型。循环内可使用 continue 跳过当前迭代进入下一次,用 break 提前终止循环。所有循环都支持嵌套和标签以精确控制外层的 breakcontinue,体现了 Rust 对安全性和表达力的兼顾。

4.3.1 理解 Rust 的基本循环:无限迭代与控制

在 Rust 中,创建重复代码块最基础的方式是使用 loop 关键字。默认情况下,这一结构会创建一个无限循环,持续执行其代码块内的内容。

请看下面的初始示例:

rust 复制代码
fn main() {
    loop {
        println!("loop");
    }
}

如果你编译并运行这个程序,它将无限地在你的终端上打印 "loop"。你需要手动停止它,通常使用 Ctrl+C。虽然无限循环有其用途(例如,在服务器或等待事件的嵌入式系统中),但大多数实际场景需要最终终止的循环。为了实现这一点,我们可以引入一个计数器和 break 语句。break 关键字会立即退出当前循环。让我们看看如何控制一个 loop

rust 复制代码
fn main() {
    let mut i = 0; // Declare a mutable counter, initialized to 0
    loop {
        println!("loop {}", i); // Print the current value of the counter
        i += 1; // Increment the counter

        if i > 5 { // Condition to exit the loop
            break;     // Execute break to exit the loop
        }
    }
    // Execution continues here after the loop breaks
    println!("Loop finished.");
}

在这个修改后的示例中:

  • let mut i = 0; 我们声明了一个变量 i,并将其标记为 mut(可变的),因为它的值将在循环内发生变化。

  • i += 1; 在每次迭代中,我们对 i 进行自增。

  • if i > 5 { break; } 这是我们的终止条件。当 i 变为 6 时(即 i > 5 为真),break 语句被执行,循环停止。

这个受控循环的输出将是:

rust 复制代码
loop 0
loop 1
loop 2
loop 3
loop 4
loop 5
Loop finished.

该循环针对从 0 到 5(含 5)的 i 值执行。当 i 为 5 时,打印 "loop 5",i 增加到 6,条件 i > 5 变为真,循环终止。

4.3.2 条件执行:掌握 Rust 中的 while 循环

另一种控制循环执行的常见方式是使用 while 循环。只要指定的布尔条件保持为真,while 循环就会继续执行其代码块。条件在每次迭代之前进行检查。

以下示例实现了与我们之前受控的 loop 相同的结果:

rust 复制代码
fn main() {
    let mut i = 0; // Re-initialize or use a new counter
    while i <= 5 { // Loop continues as long as i is less than or equal to 5
        println!("while loop {}", i);
        i += 1; // Increment the counter
    }
    println!("While loop finished.");
}

while i ≤ 5 这一行规定了只要 i 小于或等于 5,循环就会运行。一旦 i 变成 6,条件 6 ≤ 5 的结果为 false,循环终止。

输出将是:

rust 复制代码
while loop 0
while loop 1
while loop 2
while loop 3
while loop 4
while loop 5
While loop finished.

4.3.3 轻松迭代:探索 Rust 中的 for 循环与范围

Rust 的 for 循环特别适合用于迭代一系列项,例如数字范围或集合中的元素。对于这些任务,相比于使用 loopwhile 手动管理索引,这种方式通常更符合语言习惯且更安全。

要进行指定次数的迭代,可以将 for 循环与范围结合使用:

rust 复制代码
fn main() {
    for i in 0..6 { // Iterates from 0 up to (but not including) 6
        println!("for loop {}", i);
    }
    println!("For loop (exclusive range) finished.");

    // For an inclusive range (0 to 5 inclusive):
    for i in 0..=5 {
        println!("for loop inclusive {}", i);
    }
    println!("For loop (inclusive range) finished.");
}
  • 0..6 **:**这会创建一个从 0 开始到 6 结束但不包含 6 的范围。因此,它包含数字 0、1、2、3、4 和 5。

  • 0..=5 **:**这种语法创建一个包含两端在内的范围,意味着它包含 0、1、2、3、4 和 5。

第一个 for 循环(0..6)的输出:

rust 复制代码
for loop 0
for loop 1
for loop 2
for loop 3
for loop 4
for loop 5
For loop (exclusive range) finished.

对于第二个for循环(0..=5):

rust 复制代码
for loop inclusive 0
for loop inclusive 1
for loop inclusive 2
for loop inclusive 3
for loop inclusive 4
for loop inclusive 5
For loop (inclusive range) finished.

在这个特定情况下,两者生成的数字序列是相同的。

4.3.4 Rust 中遍历数组:索引访问 vs. 直接访问

Rust 中的数组是相同类型元素的固定大小集合。for 循环提供了遍历数组的便捷方式。

首先,我们声明一个数组:

rust 复制代码
let arr = [10, 20, 30, 40, 50];

方法1:基于索引的 for 循环

你可以使用数组的索引对其进行遍历,这种方式与其他语言类似。

rust 复制代码
fn main() {
    let arr = [1, 2, 3, 4, 5];
    let n: usize = arr.len(); // Get the length of the array.

    for i in 0..n { // Iterate from index 0 to n-1
        println!("arr index: {}, value: {}", i, arr[i]); // Access element by index
    }
}
  • arr.len():此方法返回数组中的元素个数。

  • let n: usize:在 Rust 中,数组的长度(以及索引)的类型是 usize。该类型与平台相关,并且足够大,足以表示内存中任何集合的大小。

  • arr[i]:此语法用于访问数组中索引 i 处的元素。

输出将是:

rust 复制代码
arr index: 0, value: 1
arr index: 1, value: 2
arr index: 2, value: 3
arr index: 3, value: 4
arr index: 4, value: 5

方法2:直接遍历元素(更符合 Rust 风格的方式)

一种更符合 Rust 风格且通常更受青睐的遍历数组的方法是直接访问其元素:

rust 复制代码
fn main() {
    let arr = [1, 2, 3, 4, 5];

    for element_value in arr { // Iterates directly over the values in 'arr'
        println!("arr value: {}", element_value);
    }
}

此循环遍历 arr 的每个元素,在每次迭代中将其值赋给 element_value。对于包含实现了 Copy trait 的简单类型(如整数)的数组,这种迭代的行为就像是操作元素的副本,原始数组仍然可用。

此方法的输出为:

rust 复制代码
arr value: 1
arr value: 2
arr value: 3
arr value: 4
arr value: 5

这种直接迭代通常更不容易出错,因为它避免了手动管理索引和潜在的差一错误。

4.3.5 使用向量:Rust 的 for 循环、所有权和 iter()

向量(Vec<T>)是 Rust 中动态大小、可增长的集合,类似于可以改变大小的数组。遍历向量引入了一个重要的 Rust 概念:所有权。

Rust 的所有权是一套编译时的内存管理规则,核心思想是:每个值都有唯一一个变量作为它的"所有者";同一时刻只能有一个所有者;当所有者离开作用域,该值会被自动销毁并释放内存。所有权可以通过赋值或函数调用进行转移(称为"移动"),移动后原变量不能再被使用,这避免了内存的重复释放。这种机制在无需垃圾回收器的情况下保证了内存安全,从根本上防止了悬垂指针和数据竞争问题。

让我们声明一个向量:

rust 复制代码
let v = vec![10, 20, 30, 40, 50];

直接遍历与所有权

如果你像这样使用 for 循环直接遍历一个向量:

rust 复制代码
fn main() {
    let v = vec![10, 20, 30, 40, 50];

    // First loop:
    for n_val in v { // This loop takes ownership of 'v'
        println!("vec {}", n_val);
    }

    // Attempting to use 'v' again will cause a compile-time error:
    // println!("Vector length after loop: {}", v.len()); // ERROR: value used here after move
    // for n_val in v { // ERROR: value used here after move
    //     println!("vec again {}", n_val);
    // }
}

当你编写 for n_val in v 时,Rust 会隐式地在向量 v 上调用一个名为 into_iter() 的方法。into_iter() 方法会消耗向量,获取其所有权。这意味着循环结束后,向量 v 在当前作用域中不再有效;它已被"移动"到循环中,并在循环结束时被丢弃(其内存被释放)。在此循环之后任何尝试使用 v 的行为都将导致编译时错误"value used here after move",这是 Rust 防止释放后使用(use-after-free)错误的关键安全特性。

第一个(也是唯一成功的)循环的输出:

rust 复制代码
vec 10
vec 20
vec 30
vec 40
vec 50

使用 .iter() 进行多次迭代(借用)

如果你需要多次遍历一个向量,或者在循环结束后仍要使用该向量,那么你必须借用它,而不是让循环消耗它。这可以通过使用 .iter() 方法来实现。

rust 复制代码
fn main() {
    let v = vec![10, 20, 30, 40, 50];

    // First loop using .iter()
    for n_val_ref in v.iter() { // '.iter()' borrows 'v'
        println!("vec ref {}", n_val_ref); // n_val_ref is a reference (e.g., &i32)
    }

    // Second loop using .iter() - this is now allowed because 'v' was only borrowed
    for n_val_ref in v.iter() {
        println!("vec ref again {}", n_val_ref);
    }

    println!("Vector is still valid, length: {}", v.len());
}
  • v.iter():此方法返回一个迭代器,该迭代器生成对向量中元素的引用(例如,如果 v 包含 i32,则生成 &i32)。原始向量 v 仅在循环期间被借用,并且在循环结束后仍然有效,由其原始作用域所拥有。

  • 由于 n_val_ref 是一个引用,如果你需要实际的值,可能需要对其进行解引用(例如 *n_val_ref),尽管 println! 通常出于显示目的会自动处理这一点。

输出将是:

rust 复制代码
vec ref 10
vec ref 20
vec ref 30
vec ref 40
vec ref 50
vec ref again 10
vec ref again 20
vec ref again 30
vec ref again 40
vec ref 50
Vector is still valid, length: 5

理解 into_iter()(获取所有权)和 iter()(借用)之间的区别对于在 Rust 中有效处理集合至关重要。如果你需要在循环内修改元素,则应使用 .iter_mut(),它提供可变引用。

Rust 的所有权系统通过三条核心规则在编译时管理内存:每个值都有唯一的所有者;所有者离开作用域时值被自动释放;值可以通过移动(所有权转移)或借用(临时使用)来共享。借用允许使用引用(&T&mut T)访问值而不获取所有权,且编译器强制遵循严格的借用规则------要么同时存在多个不可变引用(&T),要么只有一个可变引用(&mut T),绝不允许两者共存------从而在无需垃圾回收的情况下保证内存安全、防止数据竞争,同时保持高性能。

4.3.6 强大的循环:Rust 中从 loop 表达式返回值

在 Rust 中,loop 是一个表达式,这意味着它可以产生一个值。通过在 break 关键字后跟一个表达式(例如 break 42),就可以将指定的值作为循环的返回值,并赋值给变量(如 let x = loop { break 42; };)。当循环执行到 break 时,循环立即终止,并返回该值。这种方式非常适合在需要循环计算结果但又不想引入额外可变状态时使用,且所有 break 返回的值必须具有相同的类型,以保证类型安全。

要从循环返回的值是在 break 关键字之后指定的。

rust 复制代码
fn main() {
    let mut i = 0;
    let result_string: &str = loop { // The loop expression is assigned to 'result_string'
        println!("loop computation {}", i);
        i += 1;
        if i > 5 {
            break "loop computation ends here"; // Return this string literal
        }
    }; // Note the semicolon: `let ... = loop { ... };` is a statement.

    println!("Loop returned: {}", result_string);
}

此示例中的关键点:

  • let result_string: &str = loop { ... };:整个 loop { ... } 块是一个表达式。它的结果值被赋给 result_string

  • result_string 的类型被显式注解为 &str(一个字符串切片引用),因为该循环被设置为返回一个字符串字面量。Rust 通常可以推断出这一点,但在此处显式注解是一个好习惯。

  • break "loop computation ends here";:当 i 超过 5 时,循环终止。break 语句不仅退出循环,还提供了值 "loop computation ends here",该值成为整个 loop 表达式的结果。

  • 分号:由于 let result_string = ...; 是一个声明语句,因此 loop 块(当用作赋值中的表达式时)后面必须跟一个分号。

此代码的输出将是:

rust 复制代码
loop computation 0
loop computation 1
loop computation 2
loop computation 3
loop computation 4
loop computation 5
Loop returned: loop computation ends here

这种 loop 能够返回值的能力使得代码更加简洁且富有表现力,尤其是当循环的主要目的是计算一个后续需要使用的结果时。请记住,只有 loop 关键字支持通过 break 返回值;而 whilefor 循环则不能直接以这种方式返回值(它们会求值为 (),即单元类型)。

4.4 循环练习

练习1:返回 nums 向量中所有整数的总和。

rust 复制代码
pub fn sum(nums: Vec<i32>) -> i32 {
    let mut sum: i32 = 0;

    for element_value in nums {
        sum += element_value;
    }

    return sum;
}

fn main() {
    let nums: Vec<i32> = vec![10, 20, 30, 40, 50];
    let sum: i32 = sum(nums);
    println!("向量nums的元素和为:{sum}");
}

练习2:返回一个长度为 n 的向量,其中每个元素都是值 i。

rust 复制代码
pub fn fill(i: u32, n: usize) -> Vec<u32> {
    // let v: Vec<u32> = vec![i; n];
    // return v;

    let mut v: Vec<u32> = Vec::new();
    let n: u32 = n as u32;

    for _ in 1..n+1 {
        v.push(i);
    }

    return v;
}

fn main() {
    let i: u32 = 1;
    let n: usize = 5;
    let v: Vec<u32> = fill(i, n);

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

4.5 测试

4.6 match

4.6.1 掌握 Rust 的 match 控制流

在 Rust 中,控制流不仅限于 if/else 语句和循环。其中一个最强大且常用的结构是 match 关键字。虽然它看起来可能像其他语言中的 switch 语句,但 match 是一个功能更强大的表达式,它支持健壮的模式匹配,确保你处理每一种可能的情况,这是 Rust 安全保证的基石。

4.6.2 基本 match 语法

match 的核心是允许你将一个值与一系列模式进行比较。当它找到一个匹配的模式时,就会执行与该模式关联的代码。每个模式及其关联的代码被称为一个"分支"。

让我们看一个基本示例。这里,我们想根据整数 x 的值执行不同的操作。

rust 复制代码
fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
    }
}

在这段代码片段中,我们正在对 x 的值进行匹配。match 表达式会按顺序检查每个分支。由于 x 的值是 1,它会匹配第一个分支 1 ⇒ println!("one"),于是程序打印出 "one"。

4.6.3 穷尽性规则

如果你尝试直接编译上面的代码,会遇到一个编译器错误。这是因为 Rust 中的 match 必须是穷尽的 。这意味着你必须为类型可能持有的每一个值都提供一个分支。我们的变量 xi32 类型(32 位整数),但我们只处理了值 1、2 和 3。

编译器会停止编译并给出一条有用的提示信息:

rust 复制代码
error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `4_i32..=i32::MAX` not covered
 --> src/main.rs:5:11
  |
5 |     match x {
  |           ^ patterns `i32::MIN..=0_i32` and `4_i32..=i32::MAX` not covered

为了满足编译器的要求,并使我们的代码更加健壮,我们需要一种方法来处理所有其他可能的值。这可以通过使用特殊的通配符模式 _ 来实现,它充当一个"兜底"分支。

rust 复制代码
fn main() {
    let x = 5; // Changed to demonstrate the catch-all

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        // The wildcard `_` handles all other possible values.
        _ => println!("others"),
    }
}

现在,如果 x 是除 1、2 或 3 之外的任何值,最后一个分支将会执行,打印出 "others"。这种穷尽性检查是 Rust 的一个关键特性,它通过强制你考虑所有可能的结果来防止错误。

4.6.4 高级模式匹配

match 的强大之处不仅限于简单的值检查。你可以匹配多个值、范围,甚至可以将匹配到的值绑定到一个新变量,以便在分支的表达式内部使用。

匹配多个值和范围

你可以使用 |(或运算符)让一个分支处理多个模式。要匹配一个连续的值序列,你可以使用包含范围的语法 ..=

rust 复制代码
fn main() {
    let x = 7;

    match x {
        // This arm matches if x is 1, 2, OR 3.
        1 | 2 | 3 => println!("1, 2, or 3"),
        // This arm matches any number from 4 to 10, inclusive.
        4..=10 => println!("4 to 9"),
        _ => println!("others"),
    }
}

在这个示例中,由于 x 是 7,它落入了 4...=10 范围,程序将打印 "4 to 9"。

4.6.5 使用 @ 绑定匹配到的值

有时你希望匹配一个范围,但同时需要使用被匹配到的具体值。你可以使用 @ 符号将值绑定到一个变量。

rust 复制代码
fn main() {
    let x = 10;
    match x {
        // 'i' will be bound to the value of x if it's in the range 1..=10.
        i @ 1..=10 => println!("1 to 10: found {}", i),
        _ => println!("others"),
    }
}

在这里,x 匹配了范围 1..=10x 的值(即 10)被绑定到变量 i 上,然后我们可以在 println! 宏中使用它。输出将是:1 to 10: found 10

4.6.6 常见用例:处理 OptionResult

match 最符合语言习惯且最强大的用法是处理枚举,尤其是标准库中的 Option<T>Result<T, E> 类型。match 强制你处理枚举的每一个变体,从而使你的代码更安全。

1、匹配 Option<T>

一个 Option 可以是包含值的 Some(value),也可以是表示没有值的 Nonematch 是安全地解包它的完美工具。

rust 复制代码
fn process_optional(x: Option<i32>) {
    match x {
        // If x is Some, the inner value is bound to `val`.
        Some(val) => println!("Option contains the value: {val}"),
        // Handles the case where x is None.
        None => println!("Option is None"),
    }
}

fn main() {
    process_optional(Some(9));
    process_optional(None);
}

2、匹配 Result<T, E>

类似地,Result 要么代表成功 Ok(value),要么代表失败 Err(error)。使用 match 是显式处理这两种结果的规范方式。

rust 复制代码
fn process_result(res: Result<i32, String>) {
    match res {
        // Handles the success case and binds the value to `val`.
        Ok(val) => println!("Success! The value is {val}"),
        // Handles the error case and binds the error to `err`.
        Err(err) => println!("Error: {err}"),
    }
}

fn main() {
    process_result(Ok(123));
    process_result(Err("failed to process data".to_string()));
}

4.6.7 将 match 用作表达式

在 Rust 中,大多数控制流结构都是表达式,这意味着它们会求值为一个值。match 也是如此。你可以使用一个 match 块来确定一个变量的值。返回的值是执行的那个分支中表达式的结果。

这对于转换数据或提供默认值来说非常有用。

rust 复制代码
fn main() {
    let x: Option<i32> = Some(9);
    // let x: Option<i32> = None; // Try uncommenting this line

    // The result of the match block is assigned to `z`.
    let z: i32 = match x {
        Some(val) => val, // If Some, return the inner value.
        None      => 0,   // If None, return a default value of 0.
    };

    println!("The value of z is: {z}");
}

如果 xSome(9),第一个分支执行,表达式 val(即 9)被返回并赋值给 z。如果 xNone,第二个分支将执行,返回 0。这种模式提供了一种干净且安全的方式从 Option 中获取值,并为 None 情况提供了备选。

4.7 match练习

练习1:num 转换为字符串,1 对应 3。如果 num 大于 3,则返回 "other"。

rust 复制代码
pub fn num_to_string(num: u32) -> String {
    match num {
        1 => return "three".to_string(),
        _ => return "other".to_string(),
    }
}

fn main() {
    let num: u32 = 1;

    let num_str: String = num_to_string(num);
    println!("{num_str}");
}

练习2:提取 Some 中包裹的值。如果 x None ,则返回默认值 v

rust 复制代码
pub fn unwrap_or_default(x: Option<u32>, v: u32) -> u32 {
    match x {
        Some(val) => return val,  // val代表 Some的具体值
        None => return v,
    }
}

fn main() {
    let x: Option<u32> = None;
    let v: u32 = 2;
    let num_str:u32 = unwrap_or_default(x,v);
    println!("{num_str}");
}

4.8 测试

4.9 if...let语句

4.9.1 在 Rust 中使用 if let 简洁处理单模式匹配

当在 Rust 中处理枚举(如 Option 类型)时,match 语句是处理所有可能变体的强大工具。然而,在某些场景下,你只对某一个特定的变体感兴趣,而想忽略其他变体。在这种情况下,完整的 match 语句可能会显得冗长。Rust 为这些情况提供了一个更简洁的构造:if let

4.9.2 回顾 match 在 Option 类型上的应用

让我们先看一个常见用例:处理 Option<i32>Option 枚举可以是 Some(value)(表示存在值),也可以是 None(表示没有值)。

考虑下面这段使用 match 语句检查 Option<i32> 的代码:

rust 复制代码
fn main() {
    let x: Option<i32> = Some(9);
    match x {
        Some(val) => println!("Option is {val}"), // If x is Some, bind its value to val and print
        None => {}                                // If x is None, do nothing
    }
}

分析这个示例:

  • let x: Option<i32> = Some(9);:我们声明了一个类型为 Option<i32> 的变量 x,并将其初始化为 Some(9)

  • match x { ... }:这开始了一个对 x 值进行匹配的 match 表达式。

    • Some(val) ⇒ println!("Option is {val}");:这是 match 的第一个分支。如果 xSome 变体,其内部值(在此例中是 9)会被绑定到变量 val 上。然后代码执行 println!("Option is {val}")

    • None ⇒ {}:这个分支处理 xNone 的情况。空的花括号 {} 表示一个空代码块,意味着如果 xNone,则不执行任何操作。

在这个特定的 match 结构中,我们主要关心的是 Some(val) 这一情况。None 情况虽然被显式处理了,但实际上没有任何操作,这感觉像是一种样板代码。

4.9.3 引入 if let 以提高简洁性

对于只需要匹配单个模式而忽略所有其他模式的情况,Rust 提供了 if let 结构。它可以被看作是仅针对一个特定模式执行代码的 match 语句的语法糖。

让我们用 if let 重写之前的 match 语句:

rust 复制代码
// Assuming x is defined as Some(9) from the previous example
let x: Option<i32> = Some(9);
if let Some(val) = x {
    println!("Option is {val}");
}

以下是 if let 的工作方式:

  • if let Some(val) = x { ... }

    • 这条语句尝试将模式 Some(val) 与表达式 x 进行匹配。

    • 如果 x 成功匹配 Some(val)(即 x 确实是 Some 变体),则 Some 中包含的值(此处为 9)会被绑定到变量 val 上。

    • 随后执行代码块 { println!("Option is {val}"); }

    • 如果 x 不匹配 Some(val) 模式(例如,如果 xNone),则模式匹配失败,关联的代码块会被直接跳过。如果你打算在 None 情况下什么都不做,就无需显式的 else 子句。

4.9.4 if let 的等效性与优势

对于给定的问题,上述 match 版本和 if let 版本在功能上是等效的:如果 xSome(9),它们都会执行 println! 宏;如果 xNone,它们都什么都不做。

if let 的主要优势在于其简洁性。它让你可以避开完整的 match 语句的样板代码,包括为你打算忽略的模式编写分支,尤其是当你的逻辑集中在单一情况时。if let 还支持解构;如果模式匹配成功,它可以提取并绑定匹配结构中的值(例如从 Some(val) 中提取 val),使其在 if 代码块中可用。

4.9.5 执行与输出确认

当包含 matchif let 两个版本(且 x 被初始化为 Some(9))的代码被编译并运行时:

rust 复制代码
cargo run --example if_let

典型输出结果为:

rust 复制代码
Compiling hello_rust v0.1.0 (...)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/examples/if_let`
Option is 9
Option is 9

此输出确认了两个结构产生了相同的结果,因为在这两种情况下 x 都是 Some(9)

4.9.6 使用 if let 的关键考虑因素

  • 何时使用 if let:当你的主要目标是匹配枚举(或单一模式)的某一个特定变体,且你不关心处理其他变体/模式,或者希望它们不执行任何操作时,请使用 if let

  • 减少样板代码:与编写包含"兜底"(_)分支或针对其他情况的空分支的完整 match 语句相比,它提供了一种更符合人体工程学的方法来处理单个重要的匹配分支。

4.9.7 if let 的更广泛适用性

虽然本课主要使用 Option<i32> 来说明 if let,但它的用途并不仅限于 Option 类型。if let 可以有效地与任何枚举一起使用,包括 Result<T, E>,或者在任何你对解构并针对单一模式执行操作感兴趣的场景中使用。

例如,在处理 Result

rust 复制代码
let result_value: Result<i32, String> = Ok(10);
if let Ok(value) = result_value {
    println!("Success: {}", value);
}
// // If result_value were Err(...), the block would be skipped.

总之,if let 是 Rust 中用于条件模式匹配的一个非常有用的特性。当你主要关注单个成功的匹配情况时,它尤为出色,通过避免不必要的匹配分支,使代码更简洁、更易读、更精炼。

4.10 if...let练习

练习1:if let 语法提取存储在 Some 中的值。返回内部值。如果 xNone,则返回默认值 v

rust 复制代码
pub fn unwrap_or_default(x: Option<u32>, v: u32) -> u32 {
    if let Some(val) = x{
         val
    }else{
        v
    }
}
fn main() {
    let x: Option<u32> = None;
    let v: u32 = 2;
    let num_str:u32 = unwrap_or_default(x,v);
    println!("{num_str}");
}

4.11 测试

相关推荐
IT 行者2 小时前
CentOS 下源码编译安装完整版 Redis 8.0 指南(附 Rust 工具链详解)
redis·rust·centos
551只玄猫2 小时前
【数学建模 matlab 实验报告3】
开发语言·数学建模·matlab·课程设计·实验报告
Oll Correct2 小时前
实验十一:地址解析协议ARP不能跨网络直接使用
网络·笔记
小樱花的樱花2 小时前
C++访问权限:封装的艺术
开发语言·c++
bcbobo21cn2 小时前
C#使用一维数组作为参数传递
开发语言·数据库·c#·一维数组
老虎06272 小时前
LeetCode热题100 刷题笔记(第五天)多维动态规划(中心扩展法) 「 最长回文子串」
笔记·leetcode·动态规划
yuanlaile2 小时前
想转后端,java和go学哪个更好?
java·开发语言·golang
盐水冰2 小时前
【WEB模型】CS架构&BS架构&HTML&CSS&JS
开发语言·前端·javascript
阿凤212 小时前
js文件怎么引入到vue3的项目中
开发语言·前端·javascript·vue.js