4、函数与控制流:函数定义与参数、返回值、语句与表达式、if/else、loop、while、for循环,以及match基础用法
Rust 的函数与控制流是构建程序逻辑的骨架。与许多语言不同,Rust 将“语句”与“表达式”严格区分,这一设计直接影响了函数返回值的写法、控制流分支的取值方式,以及 match 匹配的表达能力。理解这一章的核心,不仅是为了写出能运行的代码,更是为了建立“以表达式驱动逻辑”的 Rust 思维。本章将从函数定义出发,逐步拆解参数传递、返回值机制,再深入 if/else、loop、while、for 与 match 的底层行为,最后通过一个综合案例串联所有知识点。
上图展示了本章的核心知识框架:函数定义是基础,语句与表达式的区分贯穿所有控制流,三种循环各有适用场景,match 则提供了模式匹配的强表达力。下面我们逐一深入。
4.1 函数定义与参数:类型安全与不可变绑定
Rust 函数使用 fn 关键字声明,参数必须显式标注类型,返回值类型用 -> 指定。函数体是一个块表达式,最后一个不带分号的表达式即为返回值。若函数没有返回值,则隐式返回单元类型 ()。
fn add(x: i32, y: i32) -> i32 {
x + y // 没有分号,作为返回值
}
fn print_sum(x: i32, y: i32) {
println!("Sum: {}", x + y); // 没有返回值,隐式返回 ()
}
参数默认是不可变绑定,若需修改参数,必须使用 mut 关键字。Rust 没有函数重载,但支持泛型来复用逻辑。函数参数传递遵循所有权规则:Copy 类型(如整数、浮点、布尔)按值复制,非 Copy 类型(如 String、Vec)默认移动所有权。
&T 借用而非所有权转移,是 Rust 中常见的性能优化手段。
4.2 语句与表达式:Rust 的求值哲学
Rust 是“表达式语言”,这意味着大多数代码都是表达式并产生值。语句分为两类:声明语句(如 let)和表达式语句(以分号结尾的表达式)。分号会丢弃表达式的值,使其变为语句。
let y = {
let x = 3;
x + 1 // 块的最后一行没有分号,整个块的值是 4
}; // y = 4
let z = {
let x = 3;
x + 1; // 有分号,块的值是 ()
}; // z = ()
这一机制对控制流至关重要:if、loop、while、match 都可以作为表达式使用,其分支的最后一个表达式即为整个控制流的值。
| 语法结构 | 作为表达式 | 返回值示例 |
|---|---|---|
| if/else | 是 | 分支中最后一个表达式的值 |
| loop | 是 | break 后携带的值 |
| while | 是 | 总是返回 () |
| for | 是 | 总是返回 () |
| match | 是 | 匹配分支中最后一个表达式的值 |
4.3 if/else 与 match:条件分支的两种范式
if/else 适合简单的条件判断,条件必须是布尔类型,Rust 不会自动将非布尔值转换为布尔值。作为表达式时,所有分支必须返回相同类型。
let score = 85;
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else {
"C"
}; // grade = "B"
match 是 Rust 中最强大的控制流运算符,它要求穷尽所有可能情况,每个分支是一个模式。match 不仅支持字面量匹配,还支持范围、元组解构、守卫条件等。
let number = 7;
let description = match number {
0 => "zero",
1..=9 => "single digit",
_ => "multi digit",
}; // description = "single digit"
_ 通配符可以匹配剩余所有情况,但应谨慎使用,以免掩盖未处理的逻辑分支。
4.4 三种循环:loop、while 与 for
loop 是无限循环,通常配合 break 退出。break 可以携带一个值作为 loop 表达式的返回值,这在实现“循环直到满足条件并返回结果”时非常有用。
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // result = 20
}
};
while 在条件为真时重复执行,适合不确定次数的循环。但 Rust 社区更推荐使用 for 配合迭代器,因为 for 更安全、更简洁,且不易出现索引越界。
let mut n = 3;
while n > 0 {
println!("{}", n);
n -= 1;
}
// 推荐使用 for 遍历范围
for i in 0..3 {
println!("{}", i);
}
for 循环本质上是迭代器语法糖,可以遍历数组、切片、Vec、HashMap 等。使用 enumerate() 可以同时获取索引和值。
let arr = [10, 20, 30];
for (index, value) in arr.iter().enumerate() {
println!("arr[{}] = {}", index, value);
}
4.5 综合案例:命令行计算器
下面通过一个简单的命令行计算器,综合运用函数、match、loop 和表达式。该程序接收用户输入的两个数字和运算符,输出计算结果,并支持连续计算直到用户输入 "quit"。
use std::io;
fn calculate(op: char, a: f64, b: f64) -> f64 {
match op {
'+' => a + b,
'-' => a - b,
'*' => a * b,
'/' => {
if b == 0.0 {
println!("Error: Division by zero");
f64::NAN
} else {
a / b
}
}
_ => {
println!("Unknown operator: {}", op);
f64::NAN
}
}
}
fn main() {
loop {
let mut input = String::new();
println!("Enter expression (e.g., 3 + 4) or 'quit':");
io::stdin().read_line(&mut input).expect("Failed to read");
let input = input.trim();
if input == "quit" {
break;
}
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() != 3 {
println!("Invalid format. Use: number operator number");
continue;
}
let a: f64 = match parts[0].parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid number: {}", parts[0]);
continue;
}
};
let op: char = match parts[1].chars().next() {
Some(c) => c,
None => {
println!("Missing operator");
continue;
}
};
let b: f64 = match parts[2].parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid number: {}", parts[2]);
continue;
}
};
let result = calculate(op, a, b);
if !result.is_nan() {
println!("Result: {}", result);
}
}
println!("Goodbye!");
}
这个案例展示了:loop 作为主循环、match 处理运算符和解析错误、函数封装计算逻辑、表达式返回值驱动流程。通过 continue 跳过错误输入,break 退出循环。
4.6 常见误区与总结复盘
| 误区 | 说明 | 正确做法 |
|---|---|---|
| 忘记返回值类型 | 函数体最后一个表达式没有分号,但函数签名未标注返回类型 | 明确标注 -> 类型,或使用 () |
| if 分支类型不一致 | if 作为表达式时,各分支返回不同类型 | 确保所有分支返回相同类型,或使用 match |
| 滥用 while 遍历 | 使用 while 和索引遍历集合,容易越界 | 使用 for + iter() 或 enumerate() |
| match 未穷尽 | 遗漏某些模式导致编译错误 | 添加 _ => 通配分支 |
| loop 无退出条件 | 无限循环且没有 break | 确保有明确的 break 条件或返回值 |
总结本章要点:函数是逻辑封装的基本单元,参数和返回值必须类型明确;语句与表达式的区分是 Rust 的核心特性,理解它才能写出简洁的代码;if/else 适合简单分支,match 适合复杂模式匹配且必须穷尽;loop 用于无限循环,while 用于条件循环,for 用于迭代遍历。掌握这些基础后,你将能够构建结构清晰、类型安全的程序逻辑。