Rust流程控制(上):if_else与match模式匹配

《Rust流程控制(上):if/elsematch模式匹配》

引言:引导程序的决策之路

程序的核心在于执行逻辑和做出决策。一个程序如果只能从头到尾顺序执行一系列指令,其能力将受到极大限制。为了构建能够响应不同输入、处理不同状态的复杂应用,我们需要流程控制(Control Flow) 机制。流程控制允许我们根据特定条件来决定执行哪些代码块,或者重复执行某些代码块。

Rust提供了一系列强大而富有表现力的流程控制工具。与其他许多语言一样,它拥有基于条件判断的if/else结构和用于重复执行的循环(loopwhilefor)。但更重要的是,Rust还提供了一个极其强大和灵活的工具------match表达式,它将模式匹配(Pattern Matching)提升为语言的一等公民,为处理复杂条件和数据结构提供了无与伦比的清晰度和安全性。

本文作为流程控制主题的上篇,将专注于Rust中用于条件分支的两种核心机制:

  1. if/else表达式 :我们将深入探讨if/else不仅仅是语句,更是表达式的本质,以及如何利用这一特性编写更简洁的代码。
  2. match表达式 :我们将全面解析match的工作原理,学习如何使用模式匹配来解构和处理各种数据类型,并理解match如何通过穷尽性检查(Exhaustiveness Checking)来保证代码的完备性,从而消除大量潜在的bug。
  3. if letwhile let :学习这两种作为match语法糖的便捷结构,了解它们在处理单一模式匹配时的应用场景。

通过本文的学习,您将掌握Rust中进行条件决策的核心工具,并深刻理解模式匹配如何成为编写安全、健壮且极富表达力的Rust代码的关键。


一、 if/else表达式:不仅仅是条件分支

if/else是编程中最基本、最常见的条件控制结构。它允许你根据一个布尔表达式的值来决定执行哪一段代码。

1. 基本语法

if表达式的语法与其他语言非常相似:

rust 复制代码
// ---
// File: src/main.rs
// Cargo.toml: [dependencies] (无外部依赖)
// Rust Version: 1.73.0
// ---
fn main() {
    let number = 7;

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

    let another_number = 6;

    if another_number % 4 == 0 {
        println!("number is divisible by 4");
    } else if another_number % 3 == 0 {
        println!("number is divisible by 3");
    } else if another_number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

重要if后面的条件必须 是一个bool类型的值。Rust不会像某些语言(如C或JavaScript)那样,自动尝试将非布尔类型转换为布尔类型。例如,if number { ... }这样的代码在Rust中是无法通过编译的,你必须显式地写出条件,如if number != 0 { ... }。这体现了Rust对类型安全的严格要求。

2. if是一个表达式

在Rust中,if是一个表达式(expression) ,而不是一个语句(statement)。这意味着if块本身会计算并产生一个值。这个特性使得我们可以用if来初始化变量,从而编写出更简洁、更具函数式风格的代码。

rust 复制代码
// ---
// File: src/main.rs
// Cargo.toml: [dependencies] (无外部依赖)
// Rust Version: 1.73.0
// ---
fn main() {
    let condition = true;
    
    // 使用 if 表达式来初始化变量
    let number = if condition { 5 } else { 6 };

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

这种写法避免了声明一个可变的let mut number;,然后在ifelse块中分别给它赋值。代码更紧凑,并且number可以是不可变的,这更符合Rust的惯用法。

使用if表达式的注意事项
ifelse块中所有可能返回的值,其类型必须是相同的。因为变量在编译时必须有一个确定的类型。

rust 复制代码
// ---
// File: src/main.rs
// ---
fn main() {
    let condition = true;

    // 这段代码无法通过编译!
    // let number = if condition { 5 } else { "six" }; 
    // error[E0308]: `if` and `else` have incompatible types
    // expected integer, found `&str`
}

编译器会报错,因为它无法在编译时确定number的类型到底是整数还是字符串切片。


二、 match表达式:Rust的超级英雄

如果说if/else是进行简单布尔判断的瑞士军刀,那么match就是处理复杂条件和数据解构的"超级英雄"。match表达式允许你将一个值与一系列的模式(patterns) 进行比较,并根据匹配的模式执行相应的代码。

模式可以是由字面值、变量名、通配符以及其他多种形式构成的复杂结构,这使得match远比传统的switch语句强大得多。

1. 基本语法与工作原理

match表达式由match关键字、一个要匹配的值、以及一系列的"分支"(arms)组成。每个分支包含一个模式和一段代码,用=>符号分隔。

rust 复制代码
// ---
// File: src/main.rs
// Cargo.toml: [dependencies] (无外部依赖)
// Rust Version: 1.73.0
// ---
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

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 my_coin = Coin::Quarter;
    let value = value_in_cents(my_coin);
    println!("The value of the coin is {} cents.", value);
}

match会依次将coin的值与每个分支的模式进行比较。一旦找到第一个匹配的模式(例如,当coinCoin::Quarter时,它匹配了第四个分支),它就会执行该分支=>右侧的代码,并且不会继续检查其他分支。

2. 穷尽性检查(Exhaustiveness Checking)

match最强大的安全特性之一是穷尽性检查 。Rust编译器会确保你为所有可能的值都提供了一个匹配分支。以上面的Coin枚举为例,如果我们漏掉了一个分支:

rust 复制代码
// ---
// File: src/main.rs
// ---
/*
fn value_in_cents_incomplete(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        // 缺少了 Dime 和 Quarter
    }
}
// error[E0004]: non-exhaustive patterns: `Coin::Dime` and `Coin::Quarter` not covered
*/

编译器会拒绝编译,并给出一个非常清晰的错误,告诉你哪些模式没有被覆盖。这个特性从根本上消除了"漏掉case"这类在switch语句中常见的bug。它强制你处理所有可能性,使得代码更加健壮。

3. 模式的多样性

match的威力体现在其丰富的模式上。

a. 绑定值的模式:模式可以绑定到被匹配值的部分内容。

rust 复制代码
// ---
// File: src/main.rs
// ---
#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // ...
}

enum CoinWithState {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState), // Quarter 变体包含一个 UsState 值
}

fn value_in_cents_with_state(coin: CoinWithState) -> u8 {
    match coin {
        CoinWithState::Penny => 1,
        CoinWithState::Nickel => 5,
        CoinWithState::Dime => 10,
        CoinWithState::Quarter(state) => { // 匹配 Quarter 变体,并将其中的 state 绑定到新变量 state
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

fn main() {
    let coin = CoinWithState::Quarter(UsState::Alaska);
    value_in_cents_with_state(coin);
}

当匹配到CoinWithState::Quarter(state)时,match不仅确认了coinQuarter变体,还将Quarter内部的数据提取出来,并绑定到名为state的新变量上,该变量可以在=>右侧的代码块中使用。

b. Option<T>matchmatch是处理标准库中Option<T>枚举的理想工具,Option<T>用于表示一个值可能是某个值(Some(T))或什么都没有(None)。

rust 复制代码
// ---
// File: src/main.rs
// ---
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

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

    println!("Six: {:?}", six);   // Some(6)
    println!("None: {:?}", none); // None
}

match强制你必须同时处理SomeNone两种情况,这避免了对nullNone值的意外解引用,是Rust安全性的一个重要体现。

c. 通配符 _other

如果你不想列出所有可能的值,可以使用特殊的模式_(下划线)或一个变量名(如other)来匹配任何未被前面分支匹配到的值。

  • _(通配符):匹配任何值,但会将其绑定到变量。它用于你不在乎具体是什么值,只想忽略它的情况。
  • other(或其他变量名):匹配任何值,并将其绑定到该变量名上。
rust 复制代码
// ---
// File: src/main.rs
// ---
fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other), // 匹配其他所有数字,并将其值绑定到 other
    }

    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(), // 匹配其他所有数字,但忽略具体的值
    }
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) { println!("Moving {} spaces", num_spaces); }
fn reroll() { println!("Rerolling"); }

使用_other作为最后一个分支,可以满足match的穷尽性要求。但要注意,这可能会隐藏逻辑错误。如果你添加了一个新的枚举变体,编译器将无法再提醒你为新变体添加一个特定的处理分支,因为它会被_other分支捕获。因此,除非必要,否则显式地列出所有模式通常是更好的选择。


三、 if letwhile let:模式匹配的语法糖

有时候,你可能只关心match中的某一个分支,而其他所有情况都做同样的处理。在这种情况下,一个完整的match表达式可能会显得有些冗长。

rust 复制代码
// ---
// File: src/main.rs
// ---
fn main() {
    let config_max = Some(3u8);

    // 使用 match
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (), // 对于 None 值,什么也不做
    }
}

为了简化这种情况,Rust提供了if let语法。

1. if let

if let可以看作是match的"语法糖",它允许你将iflet结合起来,当一个值匹配某个模式时执行代码块。

rust 复制代码
// ---
// File: src/main.rs
// ---
fn main() {
    let config_max = Some(3u8);

    // 使用 if let,等价于上面的 match
    if let Some(max) = config_max {
        println!("The maximum is configured to be {}", max);
    }

    // if let 也可以有 else
    let mut count = 0;
    let coin = CoinWithState::Quarter(UsState::Alaska);
    if let CoinWithState::Quarter(state) = coin {
        println!("State quarter from {:?}!", state);
    } else {
        count += 1;
    }
}
// (CoinWithState and UsState enums need to be defined as before)
#[derive(Debug)]
enum UsState { Alaska }
enum CoinWithState { Quarter(UsState) }

if let Some(max) = config_max的含义是:"如果config_max的值是Some,那么就将Some内部的值绑定到max变量,并执行if块中的代码。"

if let失去了match的穷尽性检查,它只关心一种模式。这是一种在简洁性和穷尽性之间的权衡。当你只想匹配一种情况并忽略其他所有情况时,if let是更符合人体工程学的选择。

2. while let

类似地,while let条件循环允许你只要一个模式匹配就一直运行while循环。

rust 复制代码
// ---
// File: src/main.rs
// ---
fn main() {
    let mut stack = Vec::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);

    // 当 stack.pop() 返回 Some(value) 时,循环继续
    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
    // 当 stack.pop() 返回 None 时,循环结束
}

这个循环会不断地从stack向量中弹出元素并打印,直到pop()方法返回None(表示向量已空),此时while let的模式不再匹配,循环终止。这是一种非常优雅的处理Option返回值的循环方式。

结论:安全而富有表现力的决策

通过本文的探讨,我们掌握了Rust中进行条件分支的两种核心工具:if/elsematch

  • if/else 不仅是传统的控制流语句,更是一个表达式 ,这使得它在let绑定中非常有用,能够写出更简洁、更具声明性的代码。
  • match 表达式是Rust的"超级武器",它通过强大的模式匹配 能力和编译时的穷尽性检查,极大地提升了代码的可靠性和表达力。它强制开发者处理所有可能的情况,从而从根本上消除了许多在其他语言中常见的bug。
  • if letwhile let 作为match的便捷语法糖,为只关心单一模式的场景提供了更符合人体工程学的简洁写法。

掌握这些工具,特别是match和模式匹配的思想,是编写地道、安全、健壮的Rust代码的关键。在Rust中,你会发现自己使用match的频率远高于if/else,因为它能更好地与enumOptionResult等核心数据结构协同工作,引导你写出更可靠的程序。

在下一篇文章中,我们将继续探索流程控制的另一半------循环,学习如何使用loopwhilefor来重复执行代码。

相关推荐
小雨下雨的雨8 小时前
Rust专项——其他集合类型详解:BTreeMap、VecDeque、BinaryHeap
开发语言·后端·rust
渡我白衣8 小时前
C++世界的混沌边界:undefined_behavior
java·开发语言·c++·人工智能·深度学习·语言模型
88Ra8 小时前
Spring Boot 3.3新特性全解析
java·spring boot·后端
剑海风云8 小时前
JDK 26:HTTP/3 支持已可在 HTTP 客户端 API 中使用
java·开发语言·http
好学且牛逼的马8 小时前
【SSM框架 | day24 spring IOC 与 DI】
java·后端·spring
朝新_8 小时前
【SpringBoot】配置文件
java·spring boot·笔记·后端·spring·javaee
掘金码甲哥8 小时前
新来的外包,限流算法用的这么6
后端
叹雪飞花8 小时前
借助Github Action实现通过 HTTP 请求触发邮件通知
后端·开源·github
曾经的三心草8 小时前
springCloud二-SkyWalking3-性能剖析-⽇志上传-告警管理-接入飞书
后端·spring·spring cloud