现在位置: 首页 > Rust 教程 > 正文

Rust 闭包

Rust 中的闭包是一种匿名函数,它可以捕获并存储其所在环境中的变量。

闭包允许在定义作用域之外访问变量,并且可以在需要时将其移动或借用给闭包使用。

闭包在 Rust 中被广泛应用于函数式编程、并发编程和事件驱动编程等领域。


闭包与函数的区别

闭包和普通函数都能封装一段逻辑,但闭包多了一项核心能力——捕获外部环境中的变量。下表列出两者的主要差异:

特性闭包函数
匿名性没有名称,通常赋值给变量有固定的 fn 名称
环境捕获可以捕获外部变量不能捕获外部变量
定义方式|参数| 表达式fn 名称(参数)
类型推导参数和返回值类型通常可以省略必须显式指定参数和返回值类型
存储与传递可以作为变量、参数、返回值同样支持

闭包的声明与调用

闭包的基本语法如下:

let closure_name = |参数列表| 表达式或语句块;

参数可以有类型注解,也可以省略——Rust 编译器会根据上下文推断它们。

实例

fn main() {
    // 闭包:省略类型注解,编译器自动推断
    let add_one = |x| x + 1;
    println!("add_one(4) = {}", add_one(4)); // 输出: 5

    // 闭包:显式标注参数和返回值类型
    let multiply = |a: i32, b: i32| -> i32 { a * b };
    println!("multiply(3, 7) = {}", multiply(3, 7)); // 输出: 21

    // 多行闭包需要加大括号
    let greet = |name: &str| {
        let greeting = format!("Hello, {}!", name);
        greeting
    };
    println!("{}", greet("runoob")); // 输出: Hello, runoob!
}

闭包的调用方式与普通函数完全一致,在变量名后加括号并传入参数即可。


捕获外部变量

闭包最核心的特性是能够捕获其所在作用域中的变量。这是闭包与普通函数的本质区别。

闭包可以通过三种方式捕获外部变量:

捕获方式对应 Rust 语义说明
按引用捕获类似 &T默认行为,闭包借用变量,外部仍可使用
可变借用捕获类似 &mut T闭包需要修改变量,闭包本身须声明为 mut
按值捕获类似 T使用 move 关键字,将变量所有权移入闭包

对于实现了 Copy trait 的类型(如 i32bool),move 只会复制一份值,外部变量仍然可用。因此演示所有权转移时应选用 StringVec 等非 Copy 类型。

按引用捕获

默认情况下,闭包以不可变引用的方式借用外部变量。借用结束后,外部作用域可以继续使用该变量。

实例

fn main() {
    let text = String::from("runoob");
    // 闭包按引用捕获 text,不获取所有权
    let print_text = || println!("text = {}", text);
    print_text(); // 输出: text = runoob
    // 借用已结束,外部仍然可以使用 text
    println!("外部仍可使用: {}", text);
}

可变借用捕获

如果闭包需要修改捕获的变量,Rust 会以可变引用的方式捕获。此时闭包本身必须声明为 mut

实例

fn main() {
    let mut counter = 0;
    // 闭包以 &mut 方式捕获 counter,闭包本身也要声明为 mut
    let mut inc = || {
        counter += 1;
    };
    inc();
    inc();
    println!("counter = {}", counter); // 输出: counter = 2
}

按值捕获(move)

通过在闭包前添加 move 关键字,闭包会获取所捕获变量的所有权。所有权转移后,外部作用域将无法再使用该变量。

实例

fn main() {
    let owned = String::from("RUNOOB");
    // move 将 owned 的所有权转移进闭包
    let take_owned = move || println!("owned = {}", owned);
    take_owned(); // 输出: owned = RUNOOB
    // 若取消下面这行注释,将编译报错:owned 的所有权已被移入闭包
    // println!("{}", owned);
}

当需要将闭包传递到另一个线程或返回到作用域之外时,move 尤为常用——它确保闭包持有所需数据的所有权,不会出现悬垂引用。


闭包 Trait:Fn / FnMut / FnOnce

Rust 编译器会根据闭包捕获变量的方式,自动为闭包实现对应的 trait。理解这三个 trait 是使用闭包作为函数参数或返回值的关键。

Trait捕获方式可调用次数典型场景
Fn不可变借用(&T多次只读访问捕获变量
FnMut可变借用(&mut T多次需要修改捕获变量
FnOnce获取所有权(T仅一次消耗捕获变量,之后不可再调用

三者的继承关系为:FnFnMut 的子 trait,FnMutFnOnce 的子 trait。也就是说,一个实现了 Fn 的闭包,自动也实现了 FnMutFnOnce

注意:move 关键字只是强制获取所有权,并不意味着闭包一定是 FnOnce。如果闭包体中并没有消耗捕获的值,那么即使加了 move,闭包仍可能实现 Fn(可多次调用)。

实例

fn main() {
    let name = String::from("runoob");

    // Fn 闭包:只读取,不修改,可多次调用
    let greet = || println!("Hello, {}!", name);
    greet(); // 第一次调用
    greet(); // 第二调用,依然正常

    // FnMut 闭包:需要修改捕获变量
    let mut count = 0;
    let mut increment = || {
        count += 1;
        count
    };
    println!("increment() = {}", increment()); // 输出: 1
    println!("increment() = {}", increment()); // 输出: 2

    // FnOnce 闭包:消耗捕获的变量,只能调用一次
    let data = String::from("RUNOOB");
    let consume = move || {
        let _ = data; // 将 data 移出闭包环境,消耗了它
        println!("data has been consumed");
    };
    consume();
    // consume(); // 若取消注释,编译报错:FnOnce 闭包只能调用一次
}

闭包作为参数和返回值

闭包可以作为函数参数传入,也可以作为函数的返回值返回。这是闭包在实际开发中最常见的两种用法。

闭包作为参数

将闭包作为参数时,需要用泛型约束指定闭包的 trait 类型。以下示例中,参数 F 被约束为 Fn(i32) -> i32,表示接收一个 i32 参数并返回 i32 的闭包。

实例

// 定义一个函数,接受闭包作为参数
fn apply<F>(val: i32, f: F) -> i32
where
    F: Fn(i32) -> i32, // F 必须实现 Fn(i32) -> i32
{
    f(val)
}

fn main() {
    let double = |x| x * 2;
    let result = apply(5, double);
    println!("Result: {}", result); // 输出: Result: 10

    // 也可以直接传入匿名闭包
    let result2 = apply(3, |x| x + 100);
    println!("Result2: {}", result2); // 输出: Result2: 103
}

闭包作为返回值

由于闭包的类型是匿名的,返回闭包时需要使用 impl TraitBox<dyn Trait> 来描述返回类型。

使用 impl Fn 返回闭包

当返回的闭包类型在编译期可以确定时,使用 impl Fn 即可,无需堆分配。

实例

// 返回一个闭包,捕获参数 x 并与传入值相加
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    // 必须 move,否则 x 是局部引用,闭包返回后 x 已失效
    move |y| x + y
}

fn main() {
    let add_five = make_adder(5);
    println!("5 + 3 = {}", add_five(3)); // 输出: 5 + 3 = 8

    let add_ten = make_adder(10);
    println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}

使用 Box<dyn Fn> 返回闭包

当需要在运行时动态选择不同闭包返回时,使用 Box<dyn Fn> 将闭包分配到堆上。

实例

fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

fn main() {
    let add_ten = make_adder(10);
    println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}
方式分配位置适用场景
impl Fn编译期可确定闭包类型,性能更优
Box<dyn Fn>运行时动态选择闭包,或需要跨函数存储闭包

常见应用场景

闭包在 Rust 的日常开发中随处可见,以下是几个最典型的应用场景。

迭代器中的闭包

闭包经常与迭代器方法配合使用,用于对集合元素进行批量处理。

实例

fn main() {
    let nums = vec![1, 2, 3, 4, 5];

    // map:对每个元素进行转换
    let squared: Vec<i32> = nums.iter().map(|x| x * x).collect();
    println!("平方: {:?}", squared); // 输出: [1, 4, 9, 16, 25]

    // filter:筛选满足条件的元素
    let even: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
    println!("偶数: {:?}", even); // 输出: [2, 4]

    // fold:将集合归约为单个值
    let sum: i32 = nums.iter().fold(0, |acc, x| acc + x);
    println!("求和: {}", sum); // 输出: 15
}

闭包与多线程

在多线程编程中,闭包常用于定义线程的执行体。move 关键字在此场景下几乎是必选的,因为它将数据的所有权移入新线程,避免跨线程引用失效。

实例

use std::thread;

fn main() {
    let nums = vec![1, 2, 3, 4, 5];

    // 为每个数字创建一个线程,move 将 num 的所有权移入线程
    let handles: Vec<_> = nums.into_iter().map(|num| {
        thread::spawn(move || {
            num * 2
        })
    }).collect();

    // 等待所有线程完成并收集结果
    for handle in handles {
        let result = handle.join().unwrap();
        println!("Result: {}", result);
    }
}

闭包与错误处理

闭包可以返回 ResultOption 类型,配合迭代器方法实现简洁的错误处理逻辑。

实例

fn main() {
    let nums = vec![3, -1, 4, -5, 9];

    // 使用闭包查找第一个正数
    let first_positive = nums.iter().find(|&amp;&amp;x| x > 0);
    match first_positive {
        Some(&amp;n) => println!("第一个正数: {}", n), // 输出: 第一个正数: 3
        None => println!("没有正数"),
    }

    // 使用闭包过滤并转换,配合 Result 处理可能的错误
    let results: Vec<Result<i32, &amp;str>> = nums.iter().map(|&amp;n| {
        if n > 0 {
            Ok(n * 10)
        } else {
            Err("负数无法处理")
        }
    }).collect();

    for r in results {
        match r {
            Ok(v) => println!("成功: {}", v),
            Err(e) => println!("错误: {}", e),
        }
    }
}

性能与生命周期

闭包的性能

Rust 的闭包是轻量级的。编译器会对闭包进行内联优化,使得闭包调用的开销接近于直接调用普通函数。闭包本身不引入额外的虚函数调用或堆分配(除非显式使用 Box)。

闭包与生命周期

闭包的生命周期与它所捕获的变量密切相关。Rust 的生命周期系统确保闭包不会比它捕获的任何变量活得更长——如果闭包引用了一个局部变量,编译器会在编译期阻止你将闭包返回到该变量的作用域之外。

实例

fn main() {
    let text = String::from("runoob");

    // 正确:闭包在 text 的作用域内使用
    let print_text = || println!("{}", text);
    print_text(); // 输出: runoob

    // 如果尝试返回这个闭包,编译器会报错:
    // fn make_closure() -> impl Fn() {
    //     let text = String::from("runoob");
    //     || println!("{}", text)  // 错误:text 的生命周期不够长
    // }
    // 解决方法:使用 move 将所有权移入闭包
}

完整示例

以下示例综合演示了闭包的声明、捕获外部变量、作为参数传递等核心用法。

实例

// 定义一个函数,接受闭包作为参数
fn apply_operation<F>(num: i32, operation: F) -> i32
where
    F: Fn(i32) -> i32,
{
    operation(num)
}

fn main() {
    let num = 5;

    // 定义闭包:对数字进行平方运算
    let square = |x| x * x;

    // 将闭包作为参数传入函数
    let result = apply_operation(num, square);
    println!("Square of {} is {}", num, result); // 输出: Square of 5 is 25

    // 也可以直接传入匿名闭包
    let result2 = apply_operation(num, |x| x * x * x);
    println!("Cube of {} is {}", num, result2); // 输出: Cube of 5 is 125
}

运行该程序,输出如下:

Square of 5 is 25
Cube of 5 is 125

总结

Rust 的闭包是一种强大的抽象,它提供了一种灵活且表达力强的方式来封装逻辑。

闭包可以捕获环境变量,并且可以作为参数传递或作为返回值返回。闭包与迭代器结合使用,可以方便地实现复杂的数据处理任务。

Rust 的闭包设计兼顾了安全性、性能和生命周期——编译器会在编译期确保闭包不会引用已失效的变量,并通过内联优化保证闭包调用的零开销抽象。