4. 函数与控制流:函数定义与参数、返回值、语句与表达式、if/else分支、loop/while/for循环及模式匹配基础
Rust 的函数与控制流语法在表面上与 C 语言或 JavaScript 相似,但其底层设计逻辑却截然不同。Rust 将“表达式”作为语言的核心驱动,几乎所有产生值的结构(包括 if 分支、match 匹配、循环体)都可以作为表达式参与赋值或返回。理解这一设计哲学,是写出地道、安全且高效的 Rust 代码的关键。本章将从函数签名、参数传递、返回值机制出发,深入剖析语句与表达式的本质区别,随后系统讲解三种循环结构的使用场景与跳出策略,最后通过模式匹配基础将控制流与 Rust 的类型系统紧密结合起来。
4.1 函数定义与参数传递:类型安全的第一道防线
Rust 的函数定义使用 fn 关键字,命名遵循蛇形命名法(snake_case)。与动态语言不同,Rust 要求每个参数必须显式标注类型,这既是编译期检查的基础,也是文档化的重要手段。函数返回类型使用箭头 -> 指定,若省略则默认返回单元类型 ()。
fn add(x: i32, y: i32) -> i32 {
x + y // 注意:没有分号,这是表达式
}
fn print_hello() {
println!("Hello"); // 返回 ()
}
参数传递涉及 Rust 所有权系统的核心规则。默认情况下,参数按值传递,这意味着变量会被移动(对于堆类型)或复制(对于实现了 Copy trait 的栈类型)。如果需要借用而不转移所有权,可以使用引用 &T 或可变引用 &mut T。
核心原理:Rust 在编译期通过所有权检查器确保每个值在任何时刻只有一个所有者。函数参数传递是所有权转移的典型场景——当你将一个 String 传入函数时,原变量立即失效,除非你显式返回它或使用引用。这种设计从根本上杜绝了悬垂指针和数据竞争。
| 传递方式 | 语法 | 所有权影响 | 适用场景 |
|---|---|---|---|
| 按值传递 | fn f(x: T) | 所有权转移,原变量失效 | 需要消费该值或转移所有权 |
| 不可变引用 | fn f(x: &T) | 只读借用,原变量仍可用 | 只需读取数据,不修改 |
| 可变引用 | fn f(x: &mut T) | 可写借用,同一时刻仅一个 | 需要修改原值且不转移所有权 |
4.2 语句与表达式:Rust 的求值哲学
理解语句(statement)与表达式(expression)的区别是掌握 Rust 控制流的基石。语句是执行操作但不返回值的代码单元,以分号结尾;表达式则计算并产生一个值。Rust 中几乎所有东西都是表达式,包括函数调用、宏调用、块 { }、if 分支、match 匹配等。
let x = {
let temp = 5;
temp * 2 // 块的最后一行没有分号,作为块的值
}; // x = 10
let y = if x > 5 { 100 } else { 200 }; // if 表达式赋值
函数体本身也是一个表达式块,其最后一个表达式的值即为函数的返回值。这就是为什么 Rust 允许省略 return 关键字——隐式返回是更地道的写法。但需要注意,return 关键字仍然存在,用于提前返回。
判断方法:看代码行末尾是否有分号。有分号 → 语句,执行后丢弃结果;无分号 → 表达式,产生值。在函数末尾,无分号的表达式就是返回值;有分号则返回 ()。
4.3 if/else 分支:表达式化的条件逻辑
Rust 的 if 不是语句而是表达式,这意味着它可以直接赋值给变量。所有分支必须返回相同类型,否则编译报错。条件部分必须是 bool 类型,Rust 不会像 C 语言那样自动将非零值转换为真。
let score = 85;
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 70 {
"C"
} else {
"D"
}; // grade = "B"
这种设计让条件逻辑更加紧凑且安全。由于类型检查在编译期完成,不可能出现分支返回不同类型导致的运行时错误。此外,if 表达式也可以嵌套在更大的表达式中使用。
4.4 循环结构:loop、while 与 for 的差异化应用
Rust 提供三种循环结构,每种都有其独特定位。loop 是无限循环,通常配合 break 使用,且 break 可以携带返回值;while 在每次迭代前检查条件;for 是迭代器循环,最常用于遍历集合。
// loop 循环:适合需要反复尝试直到成功的场景
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // break 携带返回值
}
}; // result = 20
// while 循环:条件驱动
let mut n = 3;
while n > 0 {
println!("{}", n);
n -= 1;
}
// for 循环:迭代器驱动
let arr = [10, 20, 30];
for element in arr.iter() {
println!("{}", element);
}
// 范围语法
for i in 0..5 { // 0,1,2,3,4
println!("{}", i);
}
当需要跳出嵌套循环时,可以使用循环标签(label)。标签以单引号开头,放在循环前,break 或 continue 后跟标签即可指定目标循环。
'outer: for i in 0..5 {
for j in 0..5 {
if i * j > 6 {
break 'outer; // 直接跳出外层循环
}
}
}
常见误区:初学者容易混淆 while 与 loop 的使用场景。如果循环次数未知但条件明确,用 while;如果需要一个“至少执行一次”的循环或需要从循环中返回值,用 loop。另外,for 循环遍历数组时,arr.iter() 产生引用,而 arr.into_iter() 会消费数组,两者语义不同。
4.5 模式匹配基础:match 与 if let
模式匹配是 Rust 控制流中最强大的特性之一。match 表达式要求穷尽所有可能的分支,编译器会强制检查这一点,这从根本上消除了“遗漏分支”的运行时错误。每个分支由模式(pattern)和对应的表达式组成。
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
模式可以绑定值、解构元组和结构体、使用守卫条件(guard)进行额外过滤。通配符 _ 用于匹配所有剩余情况,相当于 default 分支。
let pair = (2, -2);
match pair {
(x, y) if x == y => println!("相等"),
(x, y) if x + y == 0 => println!("互为相反数"),
(x, _) if x > 0 => println!("第一个为正"),
_ => println!("其他情况"),
}
// if let 简化单分支匹配
let optional = Some(7);
if let Some(value) = optional {
println!("值为 {}", value);
} else {
println!("没有值");
}
4.6 实战案例:命令行计算器中的控制流设计
让我们通过一个简易命令行计算器来综合运用本章知识。该程序接收用户输入的两个数字和运算符,输出计算结果,并支持连续计算直到用户输入 quit。
use std::io;
fn calculate(op: char, a: f64, b: f64) -> Result<f64, String> {
match op {
'+' => Ok(a + b),
'-' => Ok(a - b),
'*' => Ok(a * b),
'/' => {
if b == 0.0 {
Err("除数不能为零".to_string())
} else {
Ok(a / b)
}
}
_ => Err(format!("不支持的运算符: {}", op)),
}
}
fn main() {
loop {
let mut input = String::new();
println!("请输入表达式 (例如: 3 + 5),或输入 quit 退出:");
io::stdin().read_line(&mut input).expect("读取失败");
let input = input.trim();
if input == "quit" {
break;
}
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() != 3 {
println!("格式错误,请使用: 数字 运算符 数字");
continue;
}
let a: f64 = match parts[0].parse() {
Ok(num) => num,
Err(_) => {
println!("第一个数字解析失败");
continue;
}
};
let b: f64 = match parts[2].parse() {
Ok(num) => num,
Err(_) => {
println!("第二个数字解析失败");
continue;
}
};
let op = parts[1].chars().next().unwrap();
match calculate(op, a, b) {
Ok(result) => println!("结果: {}", result),
Err(msg) => println!("错误: {}", msg),
}
}
println!("感谢使用,再见!");
}
这个案例展示了 loop 作为主循环、match 处理运算符分派、if 处理除零检查、break 退出循环、continue 跳过错误输入。通过 Result 枚举与模式匹配的结合,错误处理变得显式且安全。
案例复盘:该程序的核心设计决策是使用 Result<f64, String> 作为返回值,而不是直接返回 f64。这使得错误信息可以携带具体原因,调用方通过 match 强制处理成功与失败两种情况。如果使用 f64 直接返回,遇到除零时只能返回 NaN 或 inf,无法区分是用户输入错误还是计算逻辑问题。这种“用类型表达可能性”的思路是 Rust 编程的重要方法论。
4.7 常见误区与总结复盘
| 误区 | 错误示例 | 正确做法 |
|---|---|---|
| 忘记函数末尾表达式加分号 | fn f() -> i32 { 5; } | 去掉分号:fn f() -> i32 { 5 } |
| if 分支返回不同类型 | let x = if c { 1 } else { "a" }; | 确保所有分支类型一致 |
| 循环中忘记更新条件变量 | while n > 0 { println!("{}", n); } | 在循环体内修改 n,否则死循环 |
| match 分支不穷尽 | 只匹配部分枚举变体 | 添加 _ => ... 通配分支 |
误用 while 遍历集合 | 用索引遍历 Vec | 使用 for item in vec.iter() |