4. 函数与控制流:函数定义与参数、返回值、语句与表达式、if/else分支、loop/while/for循环及模式匹配基础

Rust 的函数与控制流语法在表面上与 C 语言或 JavaScript 相似,但其底层设计逻辑却截然不同。Rust 将“表达式”作为语言的核心驱动,几乎所有产生值的结构(包括 if 分支、match 匹配、循环体)都可以作为表达式参与赋值或返回。理解这一设计哲学,是写出地道、安全且高效的 Rust 代码的关键。本章将从函数签名、参数传递、返回值机制出发,深入剖析语句与表达式的本质区别,随后系统讲解三种循环结构的使用场景与跳出策略,最后通过模式匹配基础将控制流与 Rust 的类型系统紧密结合起来。

Rust 函数与控制流核心知识结构 函数定义与参数 • fn 关键字 + 蛇形命名 • 参数必须标注类型 • 按值传递 / 引用传递 • 泛型参数 <T> • 函数指针作为参数 → 所有权与借用规则约束 语句与表达式 • 语句:执行不返回值 • 表达式:产生值 • 分号抑制表达式值 • 块 { } 也是表达式 • 函数体末尾表达式即返回值 → 隐式返回,无需 return 循环与分支 • if/else 表达式分支 • loop 无限循环 + break • while 条件循环 • for 迭代器循环 • 循环标签 'outer → break 可携带返回值 模式匹配基础 • match 穷尽性检查 • 字面量 / 通配符 _ • 解构元组与结构体 • 守卫条件 if guard • if let 简化单分支 → 与枚举类型配合最佳 核心思想:一切皆表达式,类型驱动控制流,所有权贯穿函数边界

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()