rust 快速入门——7 流程控制

110 阅读12分钟

普若哥们儿

github.com/wu-hongbing…

gitee.com/wuhongbing/…

流程控制

分支

条件语句

所有的 if 表达式都以 if 关键字开头,其后跟一个条件。

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Rust 比其它语言有更严格的类型要求,if 语句中的条件必须是 bool 值。

let 语句中使用 if

因为 if 是一个表达式,我们可以在 let 语句的右侧使用它,例如:

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

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

number 变量将会绑定到表示 if 表达式结果的值上。

记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个 if 表达式的值取决于哪个代码块被执行。这意味着 if 的每个分支的可能的返回值都必须是相同类型

match

match 控制流结构

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

match 有点类似与其它语言的 switch。首先来看看 match 的通用形式:

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

这里的模式是编译原理中的概念,模式匹配的意思是 target 在语义上与 模式 所表示的句子吻合。

先来看一个关于 match 的简单例子:

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
        _ => println!("West"),
    };
}

这里我们想去匹配 dire 对应的枚举类型,因此在 match 中用三个匹配分支来完全覆盖枚举变量 Direction 的所有成员类型,有以下几点值得注意:

  • match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的其它所有可能性
  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • X|Y,类似逻辑运算符 ,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可

其实 match 跟其他语言中的 switch 非常像,_ 类似于 switch 中的 default。但是 match 在语法上保证 target所有情况均得到处理,比如:

fn main() {
    let mut a = 3;
  match a {
      1=>println!("One"),
      2=>println!("Two"),
      3=>println!("Three"),
      i32::MIN..=0_i32 | 4_i32..=i32::MAX => println!("others"),
      _ => println!("others"),
  }
}

如果没有第 7 或第 8 行,编译出错,编译器保证了变量 a 的所有可能情况均被处理,这是其它语言中的 switch 所不具备的,从这一点也能感受到 Rust 处处考虑程序的安全和完备性。

Option 枚举和其相对于空值的优势

Rust 并没有很多其他语言中有的空值功能。空值Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。

空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性无处不在,非常容易出现这类错误。

然而,空值所表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。

Rust 并没有空值,不过它拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>,它定义于标准库中:

enum Option<T> { // 通过泛型定义一个枚举
    None,
    Some(T), // 有关联数据,关联数据是任意类型
}

Option<T> 枚举类型是如此有用以至于它甚至被包含在了 prelude 之中,不需要将其显式引入作用域即可使用。另外,它的成员也是如此,可以不需要 Option:: 前缀来直接使用 SomeNone。即便如此 Option<T> 也仍是常规的枚举,Some(T)None 仍是 Option<T> 的成员。

Rust 的意图是,如果处理的某种类型的变量存在空值的情况,就将这种类型的数据放在 Option<T> 枚举里,并作为 Option:: Some 的关联数据,而 Option:: None 就作为这种类型数据的空值。Option<T> 枚举类型只有 2 个值,要么是有值,要么是空值,这样,你在编写代码时,从语法上强制你明确处理有值和空值两种情况,避免将空值作为非空值处理而引发异常。

fn main() {
    let a: Option<i32>;
    let b = false;
    a = get_data(b);
    match a {
    // 删除以下任意一句都无法编译!强制处理有值、空值两种情况
        Some(x) => println!("has data:{}",x), //打印Some(x)中的值
        None => println!("No data"),
    }
}

fn get_data(x: bool) -> Option<i32> {
    if x {
        Some(1)
    } else {
        None
    }
}

<T> 是一个泛型类型参数,<T> 意味着 Option 枚举的 Some 成员可以包含任意类型的数据,同时每一个用于 T 位置的具体类型使得 Option<T> 整体作为不同的类型。这里是一些包含数字类型和字符串类型 Option 值的例子:

fn main() {
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
}

some_number 的类型是 Option<i32>some_char 的类型是 Option<char>,这与 some_number 是一个不同的类型。因为我们在 Some 成员中指定了值,Rust 可以推断其类型。对于 absent_number,Rust 需要我们指定 Option 整体的类型,因为编译器只通过 None 值无法推断出 Some 成员保存的值的类型,例中通过 Option<i32> 明确告诉 Rust absent_numberOption<i32> 类型的。

注意, Option<T>T 是不同的类型,编译器不允许像普通值那样使用 Option<T>。例如,这段代码不能编译,因为它尝试将 Option<i8>i8 相加:

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
}

当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值,不论是负数、正数还是 0,都是一个有效的 i8 类型的值,我们可以自信使用而无需做空值检查。只有当使用 Option<i8>(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。

换句话说,只要一个值不是 Option<T> 类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。

那么当有一个 Option<T> 的值时,除了通过 matchSome 成员中取出 T 的值来使用,Option<T> 枚举还拥有大量用于各种情况的方法,可以查看它的文档

总的来说,为了使用 Option<T> 值,需要编写处理每个成员的代码。match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,并强制检查程序是否处理了所有可能的情况。

if let 简洁控制流

if let 语法让我们以一种简洁的方式结合 iflet,来处理只匹配一个模式的值而忽略其他模式的情况。if let 语法形式:

if let 模式 = 变量 { 语句 }

如果 变量 能够匹配 模式,则执行语句体。

考虑示例中的程序,它匹配一个 config_max 变量中的 Option<u8> 值并只希望当值为 Some 成员时执行代码:

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (),
    }
}

如果值是 Some,我们希望打印出 Some 成员中的值,这个值被绑定到模式中的 max 变量里。对于 None 值我们不希望做任何操作。为了满足 match 表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 _ => (),这样也要增加很多烦人的样板代码。

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

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {}", max);
    }
}

if let 语法获取通过等号分隔的一个模式和一个表达式。它的工作方式与 match 相同,这里的表达式对应 match 而模式则对应第一个分支。在这个例子中,模式是 Some(max)max 绑定为 Some 中的值。接着可以在 if let 代码块中使用 max 了,就跟在对应的 match 分支中一样。模式不匹配时 if let 块中的代码不会执行。

[!NOTE] Rust 中 let 的含义为“绑定”,因此 if let 模式 =变量 {语句} 的含义是:如果 变量 匹配 模式,则将变量 的值绑定到 模式中的变量,以便在 语句 中使用 模式 中的变量。

比如上例中,max 就是模式中的变量,它被绑定到变量 config_max 的值:3u8

使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match 强制要求的穷尽性检查。

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

可以在 if let 中包含一个 elseelse 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if letelse。可以使用这样一个 match 表达式:

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        _ => println!("Others"),
    };
}

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

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    if let Direction::East = dire {
        println!("East");
    } else {
        println!("Others");
    }
}

if let 用于简化 match 表达式的使用,省去了编写所有分支情况,并且一般与带有关联数据的枚举变量一起使用,比如 Option::Some。不会像下面这样使用,虽然语法上可行:

fn main() {
    let a = 5;
    if let a = 5 {
        println!("a is {}", a);
    }
}

循环

Rust 有三种循环:loopwhilefor

使用 loop 重复执行代码

loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到明确要求停止。

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

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

    println!("The result is {result}");
}

循环标签:在多个循环之间消除歧义

如果存在嵌套循环,breakcontinue 应用于此时最内层的循环。

可以在一个循环上指定一个 循环标签loop label),然后将标签与 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; 语句将退出外层循环。

while 条件循环

下例使用了 while

fn main() {
    let mut number = 3;

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

        number -= 1;
    }

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

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

for 迭代器循环

可以使用 while 结构来遍历集合中的元素,比如数组。例如:

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

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

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

但这个过程很容易出错;如果索引长度或测试条件不正确会导致程序 panic。例如,如果将 a 数组的定义改为包含 4 个元素而忘记了更新条件 while index < 4,则代码会 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环进行条件检查,以确定在循环的每次迭代中索引是否在数组的边界内。

作为更简洁的替代方案,可以使用 for 循环来对一个集合的每个元素执行一些代码。越界检查是有成本的,for 循环在能够在编译阶段保证不越,界执行效率更高。

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

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

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

for 循环的格式为:

for loop_variable in iterator {
    // 使用loop_variable变量
}

iterator 是一个实现了 std::iter::IntoIterator Trait 的类型的变量,从而可以从中遍历取值。Rust 中,集合数据类型通常都实现了这个 Trait。比如:

for i in 0..5 {
    println!("{}", i * 2);
}

for i in std::iter::repeat(5) {
    println!("turns out {i} never stops being 5");
    break; // 否则将永远循环
}

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

for 语句也可以加标签,如果 for 循环存在标签,则嵌套在该循环中的带此标签的 break 表达式和 continue 表达式可以退出此标签标记的循环层或将控制流返回至此标签标记的循环层的头部。比如:

'outer: for x in 5..50 {
    for y in 0..10 {
        if x == y {
            break 'outer;
        }
    }
}

for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。