3. 函数与控制流:构建程序逻辑的骨架

任何有实际价值的程序,都离不开两个基本要素:函数(将代码组织为可复用的单元)与控制流(决定代码执行的路径与顺序)。在 Rust 中,函数是模块化与抽象的基础,而控制流则赋予程序决策与循环的能力。本章将深入剖析 Rust 函数的定义、参数传递与返回值机制,并系统讲解表达式与语句的区别,随后逐一拆解 if/else、loop、while、for 四种循环结构,以及 break 和 continue 在循环中的精确控制技巧。理解这些内容,你将能够编写出结构清晰、逻辑严谨的 Rust 代码。

Rust 函数与控制流知识结构图 函数定义 • fn 关键字 + 函数名 • 参数列表: 类型标注 • 返回值: -> 类型 • 表达式作为返回值 • 语句以分号结尾 控制流 • if / else if / else • loop: 无限循环 • while: 条件循环 • for: 迭代集合 • break / continue 表达式 vs 语句 • 表达式: 有返回值 • 语句: 执行操作 • 块 {} 是表达式 • 末尾不加分号即返回 • if 也可作为表达式 核心原则与常见误区 • 所有权与借用: 函数参数传递涉及所有权转移或借用 • 返回值: 可以返回多个值(使用元组) • 循环标签: 嵌套循环中 break/continue 可指定标签 • 误区: 忘记分号导致返回值类型不匹配;无限循环无退出条件 • 性能: 循环内避免不必要的计算,利用迭代器提高效率

3.1 函数定义:从基础语法到参数传递

Rust 的函数定义以 fn 关键字开始,后跟函数名、参数列表、返回类型以及函数体。函数名采用蛇形命名法(snake_case),参数必须显式标注类型,这是 Rust 静态类型系统的核心要求。函数体由一系列语句和表达式组成,最后一个表达式的值即为函数的返回值(若没有返回值,则返回单元类型 ())。

// 无返回值函数
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

// 有返回值函数
fn add(a: i32, b: i32) -> i32 {
    a + b  // 注意:没有分号,作为表达式返回
}

// 使用 return 关键字提前返回
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        return None;
    }
    Some(a / b)
}

参数传递是函数设计的核心环节。Rust 中的参数传递遵循所有权规则:默认情况下,值会被移动(move)到函数内部,函数外部将无法再使用该变量。若希望函数只借用数据而不获取所有权,可以使用引用(&T)或可变引用(&mut T)。这一机制在保证内存安全的同时,也要求开发者明确设计数据的归属关系。

经验提醒:对于大型结构体或需要保留原数据的场景,优先使用引用传递以避免不必要的克隆开销。例如,处理字符串切片 &str 而非 String,处理数组时使用 &[T] 而非 Vec<T>。

3.2 表达式与语句:理解 Rust 的求值哲学

Rust 是一门基于表达式的语言,这一特性深刻影响了代码的编写方式。表达式(expression)会计算并产生一个值,而语句(statement)只执行操作,不产生值。函数体、循环体、if 分支等块结构({})本身也是表达式,其最后一个表达式的值即为整个块的值。

类别示例是否产生值典型用途
表达式a + b、x * 2、if cond { 1 } else { 2 }是赋值、返回值、条件计算
语句let x = 5;、println!("hi");、foo();否声明变量、执行操作
块表达式{ let y = 3; y + 1 }是(值为 y+1)局部作用域计算

理解表达式与语句的区别,对于编写简洁的 Rust 代码至关重要。例如,使用 if 作为表达式可以直接将条件结果赋值给变量:

let condition = true;
let number = if condition { 5 } else { 6 };
// number 的值为 5

这种写法比传统的先声明后赋值更加紧凑,且避免了可变变量的使用,符合 Rust 的不可变性偏好。需要注意的是,if 作为表达式时,所有分支必须返回相同类型的值,否则编译器会报错。

3.3 循环控制:loop、while 与 for 的实战应用

Rust 提供了三种循环结构,每种都有其适用场景。loop 用于创建无限循环,通常配合 break 在满足特定条件时退出;while 在每次迭代前检查条件,适合条件驱动的循环;for 则用于遍历集合或范围,是 Rust 中最常用的循环形式。

// loop 无限循环 + break 退出
let mut counter = 0;
loop {
    counter += 1;
    if counter == 10 {
        break;
    }
}

// while 条件循环
let mut n = 3;
while n > 0 {
    println!("{}", n);
    n -= 1;
}

// for 遍历范围
for i in 0..5 {
    println!("{}", i);  // 输出 0 到 4
}

// for 遍历数组
let arr = [10, 20, 30];
for element in arr.iter() {
    println!("{}", element);
}

break 和 continue 是循环控制的关键工具。break 用于立即终止当前循环,而 continue 则跳过本次迭代的剩余部分,进入下一次迭代。在嵌套循环中,可以使用循环标签(loop label)来指定 break 或 continue 作用于哪一层循环:

'outer: for i in 0..5 {
    for j in 0..5 {
        if i * j == 6 {
            break 'outer;  // 直接跳出外层循环
        }
        println!("i={}, j={}", i, j);
    }
}
风险提示:使用 loop 时务必确保存在明确的退出条件,否则程序将陷入死循环。在嵌入式或系统编程中,死循环可能导致系统无响应。建议在循环体内设置计数器或状态标志,并定期检查。

3.4 案例拆解:实现一个命令行计算器

为了综合运用本章知识,我们设计一个简单的命令行计算器,支持加、减、乘、除四种运算,并能够连续计算直到用户输入 quit 退出。该案例将展示函数定义、参数传递、循环控制以及表达式求值的完整流程。

背景与目标:用户通过标准输入输入表达式(如 3 + 5),程序解析并计算结果,然后继续等待下一个输入。输入 quit 时退出程序。

关键实现步骤:

  1. 定义 calculate 函数,接收操作数和运算符,返回计算结果。
  2. 使用 loop 创建主循环,读取用户输入。
  3. 使用 trim() 和 split_whitespace() 解析输入字符串。
  4. 匹配运算符并调用 calculate,处理除零错误。
  5. 使用 break 退出循环。
use std::io;

fn calculate(a: f64, op: char, b: f64) -> Result<f64, String> {
    match op {
        '+' => Ok(a + b),
        '-' => Ok(a - b),
        '*' => Ok(a * b),
        '/' => {
            if b == 0.0 {
                Err("Division by zero".to_string())
            } else {
                Ok(a / b)
            }
        }
        _ => Err("Invalid operator".to_string()),
    }
}

fn main() {
    println!("Simple Calculator (type 'quit' to exit)");
    loop {
        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("Failed to read line");
        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; }
        };
        match calculate(a, op, b) {
            Ok(result) => println!("Result: {}", result),
            Err(e) => println!("Error: {}", e),
        }
    }
    println!("Goodbye!");
}

结果与复盘:该程序成功实现了连续计算功能,通过 loop 保持程序运行,利用 break 响应退出指令,使用 continue 跳过格式错误的输入。在实现过程中,我们注意到 Rust 的 match 表达式非常适合处理多分支逻辑,而 Result 类型则优雅地处理了除零等异常情况。此案例展示了函数与控制流在实际项目中的协同工作方式。

3.5 常见误区与最佳实践

在编写 Rust 函数和控制流时,初学者常犯以下错误,需要特别注意:

  • 忘记分号导致返回值错误:函数体最后一个表达式若误加分号,会变成语句,导致返回类型变为 (),与声明的返回类型不匹配。
  • 无限循环无退出条件:使用 loop 时未设置 break 条件,或条件永远无法满足。
  • 所有权转移导致外部变量失效:将变量直接传入函数而未使用引用,函数外部将无法继续使用该变量。
  • 循环内重复分配:在 for 循环内创建大型数据结构,导致性能下降。应尽量在循环外分配,循环内复用。
  • 忽略 continue 的语义:在 while 循环中,continue 会跳过条件检查,直接进入下一次迭代,可能导致条件变量未更新而陷入死循环。
最佳实践总结:优先使用 for 循环遍历集合,利用迭代器方法(如 map、filter)替代显式循环;函数参数尽量使用引用类型;对于多返回值场景,使用元组或结构体;在循环中明确退出条件,并考虑使用 while let 处理模式匹配循环。

通过本章的学习,你已经掌握了 Rust 函数与控制流的核心机制。这些基础构件将支撑你后续编写更复杂的程序逻辑。在实际编码中,请时刻关注所有权规则与表达式求值的特点,这将帮助你写出既高效又安全的 Rust 代码。