第 4 章:控制流——`if`、`for` 和 `match` 的新花样

在前面几章,我们搭建了环境、理解了变量的可变性,并探索了 Rust 的数据类型。我们已经准备好了“食材”,现在是时候学习如何“烹饪”了——也就是如何根据这些数据来引导程序的执行路径。

你可能觉得 iffor 这些东西再熟悉不过了。但在 Rust 中,它们都藏着一个统一且强大的“新花样”,这个新花样是理解 Rust 编程范式的关键。这个核心思想就是:

在 Rust 中,大多数控制流结构都是表达式(Expressions),而不仅仅是语句(Statements)。

  • 语句(Statement):执行一个动作,但不返回值。例如 Java 中的 System.out.println("Hello");
  • 表达式(Expression):会计算并产生一个值。例如 5 + 6 这个表达式,会计算出值 11

在 Java 中,控制流(如 if-else)是语句。你不能写 int x = if (a > b) { 10; } else { 20; }; 这样的代码。为了实现类似功能,你只能使用三元运算符 ? :

而在 Rust 中,这种表达方式是浑然天成的。让我们一探究竟。


if-else:不仅仅是判断,更是赋值

if 表达式的语法对你来说应该非常亲切:

fn main() {
    let number = 7;

    if number < 5 {
        println!("条件为真");
    } else {
        println!("条件为假");
    }
}

但真正的“新花样”在于它的表达式特性。因为 if 是一个表达式,我们可以用它来给一个变量赋值。

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

    println!("The value of number is: {}", number); // 输出 5
}

这就像是把 Java 的 if 语句和三元运算符(condition ? 5 : 6)优雅地统一了起来。代码更简洁,意图也更清晰。

Rust 的安全承诺: 这里有一个重要的安全保证。if 的分支返回的值必须是相同的类型。下面的代码将无法编译:

// 编译不通过!
let number = if condition { 5 } else { "six" };

编译器会提示你 ifelse 分支的类型不匹配。这种在编译期就杜绝类型混乱的做法,是 Rust 安全性的又一体现。


循环:重复操作的多种姿势

Rust 提供了三种循环结构:loopwhilefor

loop:无限循环与返回值

loop 关键字会创建一个无限循环,相当于 Java 的 while(true)。你需要使用 break 关键字来跳出循环。

fn main() {
    let mut counter = 0;
    loop {
        println!("again!");
        counter += 1;
        if counter == 3 {
            break;
        }
    }
}

loop 的“新花样”在于,它也可以是一个表达式!你可以让 break 带一个值,这个值会作为 loop 表达式的返回值。这对于一些“重试”逻辑特别有用。

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2; // 当 counter 等于 10 时,跳出循环并返回 20
        }
    };

    println!("The result is {}", result); // 输出 20
}

这个功能在 Java 中没有直接的对应实现,通常需要通过一个外部变量来变相实现,远不如 Rust 的方式优雅。

while:熟悉的条件循环

while 循环和 Java 中的几乎一模一样,当条件为真时,循环继续。这里没有太多新意,是你熟悉的工具。

fn main() {
    let mut number = 3;

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

    println!("LIFTOFF!!!");
}
for:迭代的最佳选择

for 循环是 Rust 中最常用、最强大、也最安全的循环。

我们先回忆一下 Java 的 for 循环。经典的 C 风格循环 for (int i = 0; i < 10; i++) 很容易因为边界问题而出错(off-by-one error)。虽然 Java 后来引入了增强 for 循环 (for (String item : list)),这在 Rust 中是唯一的、也是最地道的方式。

Rust 的 for 循环用于遍历任何一个迭代器(Iterator)。例如,要遍历一个数字范围:

fn main() {
    // 1..4 是一个范围(Range),代表数字 1, 2, 3 (不包含 4)
    for number in 1..4 {
        println!("{}!", number);
    }
    // 如果需要包含 4,使用 1..=4
}

要遍历一个集合(比如我们上一章讲到的数组):

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

    for element in a.iter() { // .iter() 方法返回一个数组的迭代器
        println!("the value is: {}", element);
    }
}

思维转变: 彻底告别 C 风格的索引循环。Rust 的 for 循环通过迭代器抽象了遍历过程,你只需关心每个元素,而无需关心索引和边界。这不仅让代码更简洁,也从根本上消除了“差一错误”的可能性,大大提升了代码的安全性。


matchswitch 语句的终极进化形态

如果你觉得前面的都只是小打小闹,那么 match 将会让你真正感受到 Rust 的惊艳。match 是 Rust 版本的 switch,但它远比 Java 的 switch 强大、安全、也更具表现力。

我们先回忆一下 Java switch 的痛点:

  • 忘记写 break; 会导致意外的“贯穿”(fall-through)行为。
  • 必须提供 default 分支来处理所有未列出的情况。
  • 在旧版 Java 中,只能对有限的几种类型使用 switch

Rust 的 match 完美地解决了这些问题。它会将一个值与一系列的**模式(Pattern)**进行比较,一旦找到匹配的模式,就会执行对应的代码块。

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,
    }
}

match 的超能力体现在:

  1. 强制穷尽性(Exhaustiveness Checking):这是 match 最核心的安全保障。match 的分支必须覆盖所有可能的情况。 在上面的例子中,如果你漏掉了 Coin 枚举的任何一个成员,编译器会直接报错,绝不允许你的代码在运行时遇到未处理的情况。这就彻底消灭了忘记 default 导致的问题。

  2. 强大的模式匹配match 的模式远不止是简单的值。它可以是:

    • 绑定值的模式:这是最有用的功能之一!它可以匹配并提取enumstruct 内部的值。

      fn plus_one(x: Option<i32>) -> Option<i32> {
          match x {
              None => None,
              Some(i) => Some(i + 1), // 如果 x 是 Some,把里面的值提取到 i,然后加 1
          }
      }
    • 多重模式| 语法可以让你匹配多个值。 match x { 1 | 2 => println!("one or two"), ... }

    • 范围模式match x { 1..=5 => println!("one through five"), ... }

  3. _ 通配符:如果你不想列出所有情况,可以使用 _ 作为通配符,它会匹配任何未被匹配到的值,相当于 default,但意图更清晰。

if 一样,match 也是一个表达式,它的每个分支都必须返回相同类型的值。


本章小结

我们今天领略了 Rust 控制流的“新花样”。核心的收获是理解了**“万物皆表达式”**的思想。

  • if-else 成为了 Java 中 if 语句和三元运算符的统一体,更简洁也更安全。
  • loop 循环可以返回值,为重试逻辑提供了优雅的方案。
  • for 循环基于迭代器,从根本上杜绝了边界错误。
  • match 则是 switch 的“完全体”,它的穷尽性检查和强大的模式匹配能力,将一整类常见的 bug 消灭在了编译期。

我们已经学会了如何定义数据,以及如何控制程序的流向。下一步,我们将进入一个对于 Java 开发者来说至关重要的话题:如何组织代码?Java 的世界里,我们有 class。Rust 的世界里又是什么呢?

在下一章 《函数与方法——从 classstructimpl 中,我们将深入探讨 Rust 如何组织数据和行为,这将重塑你对“对象”的看法。