Rust—闭包的使用


闭包在 Rust 中提供了比标准函数更高级的抽象层次和更高的灵活性,特别适合处理那些需要访问或修改局部作用域变量的场景,而函数则更适合构建稳定、易于理解的公共API.

Rust 的闭包根据如何其捕获变量的方式被分类为 Fn, FnMut, 和 FnOnce 三类。

1. Fn 闭包

可以读取外部变量但不修改

#[test]
fn test_fn() {
    // 定义一个变量,供闭包借用
    let num = 5;

    // 定义一个Fn类型的闭包,它不可变地借用num
    let add_num = |x: i32| num + x; // 闭包体内的num是通过引用访问的,因此闭包为Fn类型

    // 调用闭包两次,验证它可以多次借用不可变地访问num
    println!("add_num with 10: {}", add_num(10)); // 输出 15
    println!("add_num with 20: {}", add_num(20)); // 输出 25
}

注释: 闭包 add_num 可以多次调用,因为它只是借用 num 而不改变它,符合 Fn 类型闭包的特征


2. FnMut 类型闭包

可以读取和修改外部变量

#[test]
fn test_fn_mut() {
    // 定义一个可变变量,供闭包借用和修改
    let mut counter = 0;

    // 定义一个FnMut类型的闭包,它可以可变地借用counter
    let mut increment_counter = || {
        counter += 1; // 闭包体内部修改了counter,因此是FnMut类型
    };

    // 调用闭包两次,每次调用都会增加counter的值
    increment_counter();
    increment_counter();
    println!("Counter after incrementing: {}", counter); // 输出 2
}

注释: 闭包 increment_counter 需要可变地借用 counter 并对其进行修改,因此它是 FnMut 类型。需要注意的是,同一时间只有一个 FnMut 类型的闭包可以修改同一个变量。


3. FnOnce 类型闭包(使用 move)

可以获取外部变量的所有权,这通常发生在需要移动所有权到另一个线程或进行消耗性操作时

#[test]
fn test_fn_once() {
    // 定义一个变量,将被闭包移动
    let message = "Hello, World!".to_string();

    // 定义一个FnOnce类型的闭包,它通过move关键字获取message的所有权
    let print_message = move || {
        println!("{}", message); // 闭包体内部移动了message,因此是FnOnce类型
    };

    // 调用闭包一次,因为message的所有权已被转移
    print_message(); // 输出 Hello, World!

    // 注意:在此之后,不能再访问message,因为它已经被移动到了闭包内部
    // println!("{}", message); // 这里会报错,因为message已经不存在于作用域中
}

注释: 通过使用 move 关键字,闭包 print_message 获取了 message 的所有权,这意味着 message 不能在闭包之外再次被使用,因此闭包是 FnOnce 类型。闭包只能被调用一次,因为一旦调用,被移动的所有权就完成了转移。


闭包和函数的差异

1. 名称与匿名性
  • 函数:在 Rust 中,函数是具有名称的实体,通过 fn 关键字定义。函数名作为其标识符,可以在代码中被明确引用。
  • 闭包:闭包则是匿名函数,没有固定的名称。它们可以在表达式中直接定义和使用,提供了一种更为灵活的函数定义方式。
2. 捕获环境
  • 函数:标准函数不直接捕获其外部作用域的变量。要访问外部变量,需要通过参数传递。
  • 闭包:闭包可以捕获其定义时周围环境的变量。这包括(Fn, FnMut, 和 FnOnce)。
3. 可变性与灵活性
  • 函数:函数的签名(包括参数和返回类型)一旦定义就不能更改,且不能直接访问定义时作用域之外的变量,除非它们作为参数被显式传递。
  • 闭包:闭包提供了更多的灵活性,可以隐式捕获外部变量,且通过不同的闭包类型(Fn, FnMut, FnOnce)控制对这些变量的访问权限。此外,闭包的参数和返回类型可以根据上下文被 Rust 自动推导,使得代码更加简洁。
4. 使用场景
  • 函数:适合于定义公共接口、模块间交互等场景,因其具有明确的命名和较稳定的接口。
  • 闭包:适用于处理短小的、临时的、或者需要直接访问定义环境中的数据的逻辑,常见于高阶函数(如映射、过滤)、异步编程、事件处理等场景。
5. 作为参数和返回值
  • 两者都可以作为其他函数的参数和返回值,体现了 Rust 中“函数作为一等公民”的特性。但闭包由于其灵活性,经常被用作高阶函数的参数,便于在运行时动态定义行为。