4、函数与控制流:函数定义与参数、返回值、语句与表达式、if/else、loop、while、for循环,以及match基础用法

Rust 的函数与控制流是构建程序逻辑的骨架。与许多语言不同,Rust 将“语句”与“表达式”严格区分,这一设计直接影响了函数返回值的写法、控制流分支的取值方式,以及 match 匹配的表达能力。理解这一章的核心,不仅是为了写出能运行的代码,更是为了建立“以表达式驱动逻辑”的 Rust 思维。本章将从函数定义出发,逐步拆解参数传递、返回值机制,再深入 if/else、loop、while、for 与 match 的底层行为,最后通过一个综合案例串联所有知识点。

Rust 函数与控制流知识结构图 函数与控制流 函数定义与参数 fn add(x: i32, y: i32) -> i32 参数:类型标注,不可变绑定 返回值:最后一个表达式 语句与表达式 语句:执行操作,无返回值 表达式:计算结果,有值 if/loop/while/match 均可作为表达式 三种循环 loop:无限循环,可 break 返回值 while:条件循环 for:迭代器遍历 match 基础用法 模式匹配:穷尽性检查 每个分支是表达式,可返回值 通配符 _ 处理剩余情况 与 if/else 相比更安全、更清晰

上图展示了本章的核心知识框架:函数定义是基础,语句与表达式的区分贯穿所有控制流,三种循环各有适用场景,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"
风险提示: match 必须穷尽所有可能,否则编译器会报错。使用 _ 通配符可以匹配剩余所有情况,但应谨慎使用,以免掩盖未处理的逻辑分支。

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 用于迭代遍历。掌握这些基础后,你将能够构建结构清晰、类型安全的程序逻辑。