在前面几章,我们搭建了环境、理解了变量的可变性,并探索了 Rust 的数据类型。我们已经准备好了“食材”,现在是时候学习如何“烹饪”了——也就是如何根据这些数据来引导程序的执行路径。
你可能觉得 if
、for
这些东西再熟悉不过了。但在 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" };
编译器会提示你 if
和 else
分支的类型不匹配。这种在编译期就杜绝类型混乱的做法,是 Rust 安全性的又一体现。
循环:重复操作的多种姿势
Rust 提供了三种循环结构:loop
、while
和 for
。
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
循环通过迭代器抽象了遍历过程,你只需关心每个元素,而无需关心索引和边界。这不仅让代码更简洁,也从根本上消除了“差一错误”的可能性,大大提升了代码的安全性。
match
:switch
语句的终极进化形态
如果你觉得前面的都只是小打小闹,那么 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
的超能力体现在:
-
强制穷尽性(Exhaustiveness Checking):这是
match
最核心的安全保障。match
的分支必须覆盖所有可能的情况。 在上面的例子中,如果你漏掉了Coin
枚举的任何一个成员,编译器会直接报错,绝不允许你的代码在运行时遇到未处理的情况。这就彻底消灭了忘记default
导致的问题。 -
强大的模式匹配:
match
的模式远不止是简单的值。它可以是:-
绑定值的模式:这是最有用的功能之一!它可以匹配并提取出
enum
或struct
内部的值。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"), ... }
-
-
_
通配符:如果你不想列出所有情况,可以使用_
作为通配符,它会匹配任何未被匹配到的值,相当于default
,但意图更清晰。
和 if
一样,match
也是一个表达式,它的每个分支都必须返回相同类型的值。
本章小结
我们今天领略了 Rust 控制流的“新花样”。核心的收获是理解了**“万物皆表达式”**的思想。
if-else
成为了 Java 中if
语句和三元运算符的统一体,更简洁也更安全。loop
循环可以返回值,为重试逻辑提供了优雅的方案。for
循环基于迭代器,从根本上杜绝了边界错误。match
则是switch
的“完全体”,它的穷尽性检查和强大的模式匹配能力,将一整类常见的 bug 消灭在了编译期。
我们已经学会了如何定义数据,以及如何控制程序的流向。下一步,我们将进入一个对于 Java 开发者来说至关重要的话题:如何组织代码?Java 的世界里,我们有 class
。Rust 的世界里又是什么呢?
在下一章 《函数与方法——从 class
到 struct
和 impl
》 中,我们将深入探讨 Rust 如何组织数据和行为,这将重塑你对“对象”的看法。