Rust中的闭包

Rust中的闭包

1.基础

基本语法

Rust 中的闭包使用 |参数| 表达式 的语法:

rust 复制代码
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 输出 6

闭包与函数的区别

  1. 类型推断:闭包不需要显式声明参数和返回值的类型(可以省略)
  2. 环境捕获:闭包可以捕获其定义作用域中的变量
  3. 语法简洁:闭包通常比函数更简洁

2.闭包的类型

Rust 闭包实现了以下 trait 之一(按灵活性递减顺序):

  1. Fn:不可变借用捕获环境
  2. FnMut:可变借用捕获环境
  3. FnOnce:获取所有权捕获环境,只能调用一次

示例:不同类型的闭包

rust 复制代码
// Fn 闭包
let print_message = |msg: &str| println!("{}", msg);
print_message("Hello, Fn!");

// FnMut 闭包
let mut count = 0;
let mut increment = || {
    count += 1;
    println!("Count: {}", count);
};
increment(); // Count: 1
increment(); // Count: 2

//FnOnce 闭包
let s = String::from("Hello");
let consume_string = move || {
    let a = s; //消耗掉s
    println!("{}", a);
    // s 在这里被移动
};
consume_string(); // 第一次调用正常
//consume_string(); // 错误!闭包已经消耗,不能再次调用

不同类型闭包的详细对比

核心区别在于 如何访问捕获的变量 以及 能否被多次调用


1.Fn 闭包(不可变借用)
  • 特点

    • 通过 不可变引用&T)捕获环境变量。
    • 可以 多次调用,不会消耗闭包自身或捕获的变量。
  • 使用场景

    • 只需读取环境变量,无需修改。
    • 需要重复调用的闭包(如迭代器操作、回调函数)。
  • 示例

    rust 复制代码
    let x = 10;
    let print_x = || println!("x = {}", x); // 不可变借用 x
    print_x(); // 可多次调用
    print_x();

2.FnMut 闭包(可变借用)
  • 特点

    • 通过 可变引用&mut T)捕获环境变量。
    • 可以 多次调用,但每次调用可能修改捕获的变量。
    • 闭包自身需要声明为 mut
  • 使用场景

    • 需要修改外部变量的闭包。
    • 状态保持或累计操作(如计数器)。
  • 示例

    rust 复制代码
    let mut count = 0;
    let mut increment = || {
        count += 1; // 可变借用 count
        println!("Count: {}", count);
    };
    increment(); // Count: 1
    increment(); // Count: 2

3.FnOnce 闭包(所有权转移)
  • 特点

    • 通过 所有权T)捕获环境变量。
    • 只能调用一次,调用后闭包和捕获的变量会被消耗。
    • 通常用于需要转移所有权的场景(如跨线程传递数据)。
  • 使用场景

    • 捕获的资源需要在闭包内被消耗(如 StringVec)。
    • 需要移动所有权的操作(如 thread::spawn)。
  • 示例

    rust 复制代码
    //FnOnce 闭包
    let data = vec![1, 2, 3];
    let consume_data = move || {
        let drop_data = data; //消耗掉data
        println!("Data: {:?}", drop_data);
        // data 在这里被移动,无法再次使用
    };
    consume_data();
    //consume_data(); // 错误!闭包只能调用一次

关键区别总结
特性 Fn FnMut FnOnce
捕获方式 不可变引用(&T 可变引用(&mut T 所有权(T
调用次数 多次 多次 仅一次
是否需要 mut 是(闭包变量需 mut
典型用途 无副作用操作 修改外部状态 消耗资源或跨线程

自动推导规则

Rust 编译器会根据闭包内部行为自动推断类型:

  1. 如果闭包 只读取 环境变量 → Fn
  2. 如果闭包 修改 环境变量 → FnMut
  3. 如果闭包 移动 环境变量(如 move 关键字且所有权发生转移)→ FnOnce

强制指定类型

在函数签名中,可以显式指定闭包类型:

rust 复制代码
fn call_fn<F: Fn()>(f: F) { f() }
fn call_fn_mut<F: FnMut()>(mut f: F) { f() }
fn call_fn_once<F: FnOnce()>(f: F) { f() }

为什么 FnOnce 是超级 Trait?
  • FnFnMut 都是 FnOnce 的子 Trait:
    • 所有 Fn 闭包也是 FnOnce(因为不可变借用不阻止所有权转移)。
    • 所有 FnMut 闭包也是 FnOnce
  • 因此,接受 FnOnce 的函数可以兼容所有闭包,但会限制调用次数。

实际应用建议
  • 优先用 Fn:除非需要修改或移动变量。
  • 线程安全 :跨线程时用 move + FnOnce 确保所有权转移。
  • 性能FnFnMut 通常零开销,FnOnce 可能涉及内存分配。

3.闭包作为函数参数

闭包作为函数参数是Rust函数式编程的重要特性,它提供了极大的灵活性。我们可以通过多种方式将闭包传递给函数,每种方式都有其适用场景和特点。

1.基本用法(静态)

最直接的方式是使用泛型和trait约束来接收闭包:

rust 复制代码
fn main() {
    let double = |x| x * 2;
    println!("{}", apply(double, 5)); // 输出 10

    let square = |x| x * x;
    println!("{}", apply(square, 5)); // 输出 25
}

fn apply<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(x)
}

特点:

  • 使用泛型参数FFn trait约束
  • 编译时静态分发,性能最优
  • 适用于大多数不需要修改捕获变量的场景

2.使用 trait 对象(动态)

当需要运行时动态分发时,可以使用trait对象:

rust 复制代码
fn main() {
    let mut counter = 0;
    let mut increment = || {
        counter += 1;
        println!("Counter: {}", counter);
    };
    call_twice(&mut increment);
    // 输出:
    // Counter: 1
    // Counter: 2
}

fn call_twice(f: &mut dyn FnMut()){
    f();
    f();
}

特点:

  • 使用dyn FnMut trait对象
  • 运行时动态分发,有一定性能开销
  • 允许修改捕获的变量(使用FnMut
  • 适用于需要动态选择闭包的场景

3.静态分发详解

实现方式
  • 通过 泛型(Generics)trait 约束 实现。
  • 编译器会为每个具体类型生成独立的代码(单态化,Monomorphization)。
  • 在编译期确定调用的具体方法。
代码示例
rust 复制代码
fn main() {
    let double = |x| x * 2;
    println!("{}", apply(double, 5));//输出:10  // 编译时生成 `apply::<double>` 专用代码
}

fn apply<F> (f:F,x:i32) -> i32
where
    F: Fn(i32) -> i32, // 静态分发
{
    f(x)
}
底层原理

编译器会为每个不同的 F 类型生成一个独立的 apply 版本:

rust 复制代码
// 编译器生成的代码示例
fn apply_for_double(f: impl Fn(i32)->i32, x: i32) -> i32 {
    f(x)  // 直接调用,无额外开销
}
使用场景
  • 需要最高性能(如数值计算、算法)
  • 类型在编译期已知(大多数泛型代码)
  • 避免运行时开销(如嵌入式系统)

4.动态分发详解

实现方式
  • 通过 trait 对象(dyn Trait 实现。
  • 使用 虚表(vtable) 在运行时查找正确的方法。
  • 在运行期决定调用的具体方法。
代码示例
rust 复制代码
fn main() {
    let mut counter = 0;
    let mut increment = || {
        counter += 1;
        println!("Counter: {}", counter);
    };
    call_twice(&mut increment); // 传递 trait 对象
    //输出:
    //Counter: 1
    //Counter: 2
}

fn call_twice(f: &mut dyn FnMut()){ // 动态分发
    f(); // 通过虚表查找调用
    f();
}
底层原理

编译器会生成一个 虚表(vtable),存储函数指针:

rust 复制代码
// 近似底层表示
struct VTable {
    call_fn: fn(*mut ()),  // 函数指针
}

let vtable = VTable { call_fn: increment_implementation };
let trait_obj = &mut increment as &mut dyn FnMut();  // 包含数据指针和虚表指针

调用时:

rust 复制代码
// 动态调用过程
(trait_obj.vtable.call_fn)(trait_obj.data);
示例解释

1.increment 是一个闭包,也是 trait 对象

rust 复制代码
let mut increment = || {
    counter += 1;
    println!("Counter: {}", counter);
};
  • increment 是一个闭包,它捕获了外部变量 counter 的可变引用
  • 这个闭包自动实现了 FnMut trait(因为它需要修改捕获的变量)
  • 当传递 &mut incrementcall_twice 时,它被强制转换为 trait 对象 &mut dyn FnMut()

2.FnMut() 是一个 trait

FnMut() 确实是 Rust 标准库中定义的 trait,它的简化定义如下:

rust 复制代码
pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
  • FnMut 是 Rust 的三个闭包 trait(FnOnceFnMutFn)之一
  • 它表示可以多次调用且可以修改其环境的闭包
  • 当你写 dyn FnMut() 时,就是在使用这个 trait 的动态分发版本

3.类型转换过程

rust 复制代码
call_twice(&mut increment);

这里发生了隐式的类型转换:

  1. increment 是一个具体的闭包类型(编译器生成的匿名结构体)
  2. &mut increment 是这个闭包类型的可变引用
  3. 当传递给期望 &mut dyn FnMut() 的函数时,Rust 会自动进行"胖指针"转换:
    • 数据指针:指向闭包数据
    • 虚表指针:指向该闭包类型的 FnMut 实现

4.动态分发机制

call_twice 函数内部:

rust 复制代码
fn call_twice(f: &mut dyn FnMut()) {
    f(); // 通过虚表查找调用
    f();
}

每次调用 f() 时:

1.通过虚表查找 call_mut 方法

2.传递闭包数据作为 &mut self 参数

3.执行实际的闭包代码

5.闭包与 trait 对象的关系

  • 所有闭包都会自动实现适当的闭包 trait(FnFnMutFnOnce
  • 当使用 dyn 前缀时,闭包可以被当作 trait 对象使用
  • 这种转换使得我们可以统一处理不同类型的闭包

6.对比静态分发

如果使用泛型而非 trait 对象,代码会是:

rust 复制代码
fn call_twice<F: FnMut()>(f: &mut F) {
    f();
    f();
}

这种静态分发版本:

  • 会为每种闭包类型生成专用代码
  • 性能更好(无运行时开销)
  • 但会导致代码膨胀(多个实例)

而动态分发版本:

  • 只有一份代码
  • 有运行时虚表查找开销
  • 但可以处理任意实现了 FnMut 的类型
使用场景
  • 需要运行时多态(如 GUI 事件处理器)
  • 处理异构集合(如 Vec<Box<dyn Draw>>
  • 实现插件系统或动态加载
概念解析
trait对象
1.基本概念

Trait 对象是通过将指针 (引用或智能指针)与 trait 结合创建的,形式为 &dyn TraitBox<dyn Trait> 等。其中 dyn 关键字明确表示这是动态分发。

2.创建Trait 对象
rust 复制代码
fn main() {
    let dog = Dog;
    let cat = Cat;
    //创建 trait 对象
    let animals: Vec<&dyn Animal> = vec![&dog, &cat];
}

trait Animal{
    fn speak(&self);
}

struct Dog;
impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

struct Cat;
impl Animal for Cat{
    fn speak(&self) {
        println!("Meow!");
    }
}
3.Trait 对象的组成

每个 trait 对象实际上由两个指针组成:

  • 数据指针:指向实际实例的数据

  • 虚表指针(vtable):指向该类型的 trait 实现方法列表

rust 复制代码
struct TraitObject {
    data: *mut (),
    vtable: *mut VTable,
}
4.使用场景
  • 异构集合:存储多种不同类型但实现相同 trait 的对象
rust 复制代码
fn make_sounds(animals: Vec<&dyn Animal>) {
    for animal in animals {
        animal.speak();
    }
}
  • 返回多种可能类型
rust 复制代码
fn get_pet(kind: &str) -> Box<dyn Animal> {
    match kind {
        "dog" => Box::new(Dog),
        "cat" => Box::new(Cat),
        _ => panic!("Unknown pet")
    }
}
  • 回调函数和事件处理
rust 复制代码
trait EventHandler {
    fn handle(&self, event: Event);
}
struct App {
    handlers: Vec<Box<dyn EventHandler>>
}
5.对象安全

不是所有 trait 都能作为 trait 对象使用。只有满足"对象安全"的 trait 才可以:

  • 不能返回 Self

  • 不能有泛型方法

  • 不能使用 Self 作为参数类型(除了接收者 self

为什么需要对象安全规则?

Trait 对象在运行时通过虚表(vtable)查找方法,这意味着编译器必须能够为 trait 的所有方法生成确定性的函数指针。如果 trait 违反了对象安全规则,编译器就无法生成有效的虚表。

对象安全的详细规则

  1. 不能返回 Self

    • 原因:Trait 对象会擦除具体类型信息,无法在运行时确定 Self 的具体类型

    • 错误例子:

      rust 复制代码
      trait Clone {
          fn clone(&self) -> Self; // 编译错误:不能返回 Self
      }
  2. 不能有泛型方法

    • 原因:泛型方法需要为每个具体类型生成单独的代码,与 trait 对象的单份代码理念冲突

    • 错误例子:

      rust 复制代码
      trait Serializer {
          fn serialize<T>(&self, value: T); // 编译错误:不能有泛型方法
      }
  3. 不能使用 Self 作为参数类型(除了接收者 self

    • 原因:同样因为类型擦除,无法在运行时确定 Self 的具体类型

    • 错误例子:

      rust 复制代码
      trait Equals {
          fn equals(&self, other: Self); // 编译错误:参数不能是 Self
      }
6.生命周期

Trait 对象可以指定生命周期:

rust 复制代码
trait Process {
    fn process(&self);
}

fn process_all<'a>(items: Vec<&'a dyn Process>) {
    for item in items {
        item.process();
    }
}

5.关键区别对比

特性 静态分发 动态分发
决定时机 编译期 运行期
性能 零开销,直接调用 有间接调用开销
代码生成 单态化(可能代码膨胀) 共享代码(无膨胀)
灵活性 编译时类型固定 运行时可替换实现
内存占用 可能较大(多份代码) 较小(虚表+指针)
典型用例 泛型函数、迭代器 trait 对象、插件系统

6.高级技巧

静态分发的优化
rust 复制代码
// 使用 `impl Trait` 简化签名
fn apply(f: impl Fn(i32)->i32, x: i32) -> i32 {
    f(x)
}
动态分发的灵活用法
rust 复制代码
// 返回 trait 对象(动态分发)
fn make_closure() -> Box<dyn Fn(i32)->i32> {
    Box::new(|x| x * 2)
}
混合使用
rust 复制代码
// 静态分发为主,动态分发备用
fn process<T: AsRef<str>>(s: T) {
    println!("{}", s.as_ref());  // 静态分发优先
}

fn process_dyn(s: &dyn AsRef<str>) {
    println!("{}", s.as_ref());  // 动态分发备用
}

总结

  • 静态分发 = 编译器生成专用代码 + 直接调用 → 高性能
  • 动态分发 = 运行时虚表查找 → 灵活性
  • Rust 允许混合使用两者,根据场景选择最优方案。

4.闭包高级用法

闭包是 Rust 中非常强大的特性,除了基础用法外,还有许多高级技巧可以写出更灵活、更高效的代码。下面介绍一些 Rust 闭包的高级用法和技巧。

1.闭包类型系统进阶

三种闭包 trait

Rust 闭包自动实现以下 trait 之一或多个:

  • FnOnce: 能调用一次,会消耗捕获的变量
  • FnMut: 能多次调用,可修改捕获的变量
  • Fn: 能多次调用,不可修改捕获的变量(共享引用)
rust 复制代码
fn main() {
    let double = |x| x * 2;
    assert_eq!(call_with_one(double), 2);
}

fn call_with_one<F>(func: F) -> i32
where
    F: Fn(i32) -> i32,
{
    func(1)
}

2.闭包捕获模式控制

强制移动捕获

使用 move 关键字强制闭包取得变量的所有权:

rust 复制代码
fn main() {
    let nums = vec![1, 2, 3];
    let owns_nums = move || {
        println!("Captured nums: {:?}", nums);
        // nums 的所有权已转移到闭包内
    };

    owns_nums();
    // println!("{:?}", nums); // 错误!nums 已被移动
}
选择性捕获

通过重构代码控制闭包捕获的内容:

rust 复制代码
fn main() {
    let mut count = 0;
    let mut inc = || {
        count += 1; // 只使用 count
        println!("Count:{}", count);
    };
    inc();
    inc();
}

3.闭包作为返回值

1.使用 impl Trait 返回闭包(更高效)
rust 复制代码
fn main() {
    let triple = make_multiplier(3);
    assert_eq!(triple(5), 15);
}

fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x * y
}
2.使用 Box<dyn Fn> 返回闭包

需要使用 Box 进行堆分配:

rust 复制代码
fn main() {
    let add5 = make_adder(5);
    assert_eq!(add5(3), 8);
}

fn make_adder(x:i32) -> Box<dyn Fn(i32) -> i32>{
    Box::new(move |y| x + y)
}
3.两种方式对比

返回闭包的两种方式(Box<dyn Fn>impl Fn)有重要的区别,主要体现在 性能、灵活性使用场景 上。以下是它们的对比:

1.静态分发
rust 复制代码
fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x * y
}

特点:

  • 栈分配(Stack Allocation):闭包直接存储在栈上,无堆分配开销。
  • 静态分发(Static Dispatch):编译器会单态化(monomorphize)闭包,调用时无运行时开销。
  • 零成本抽象:性能最优,和直接写闭包一样高效。
  • 只能返回一种闭包类型:编译器必须知道具体的返回类型(不能有多个分支返回不同的闭包)。

适用场景:

  • 性能敏感的场景(如高频调用的闭包)。
  • 闭包类型单一,不需要动态返回多种闭包。
2.动态分发
rust 复制代码
fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

特点:

  • 堆分配(Heap Allocation):闭包被 Box 分配到堆上,带来额外的内存开销。
  • 动态分发(Dynamic Dispatch):由于返回的是 dyn Fn,调用闭包时会有额外的虚函数表(vtable)查找开销。
  • 灵活性:可以返回不同类型的闭包(只要它们都实现了 Fn(i32) -> i32)。
  • 必须显式 Box:需要手动封装闭包。

适用场景:

  • 需要返回多种可能的闭包类型(例如,条件分支返回不同的闭包)。
  • 闭包的生命周期较长,需要存储在堆上(例如,作为长期存在的回调函数)。

其他:

Box<dyn Fn(i32) -> i32> 这个 返回值 一定要放在Box里面吗?

在 Rust 中,如果直接返回闭包(比如 move |y| x + y),必须使用 Box 进行堆分配 (即 Box<dyn Fn>),原因如下:

1.闭包的大小在编译时未知

Rust 要求函数的返回类型必须是固定大小的,但闭包是匿名类型,每个闭包的实际类型是编译器生成的唯一结构(无法提前知道大小)。例如:

rust 复制代码
let closure1 = |y| 5 + y;  // 编译器生成一个匿名类型 `Closure1`
let closure2 = |y| 10 + y; // 编译器生成另一个匿名类型 `Closure2`

这两个闭包的类型不同,无法直接作为函数返回值。

2.Box的作用

Box 是一个堆分配的指针,它的大小是固定的(通常是一个 usize,即 8 字节),因此可以满足返回类型Sized的要求:

rust 复制代码
Box::new(move |y| x + y) // 将闭包移到堆上,返回固定大小的指针

3.替代方案:impl Fn(无需 Box)

如果你能确保函数只返回一种闭包类型(而不是动态多态),可以用impl Fn避免堆分配:

rust 复制代码
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y  // 无需 Box,闭包直接存储在栈上
}

这种方式更高效,但不能用于返回多种闭包类型(比如条件分支返回不同的闭包)。


3.关键区别对比
特性 Box<dyn Fn> impl Fn
内存分配 堆分配(Box) 栈分配(无额外开销)
调用开销 动态分发(vtable 查找) 静态分发(直接调用)
灵活性 可返回多种闭包类型 只能返回一种闭包类型
适用场景 需要动态返回闭包 高性能、单一闭包类型

如何选择?

  1. 优先 impl Fn

    • 如果闭包类型单一,且性能重要(推荐默认使用)。
    • 例如:工厂函数返回固定逻辑的闭包。
  2. 使用 Box<dyn Fn>

    • 需要返回 多种不同的闭包(如条件分支返回不同闭包)。
    • 闭包需要长期存储(如作为回调存储在数据结构中)。

总结

  • impl Fn 更快,更符合零成本抽象,适用于固定闭包类型。
  • Box<dyn Fn> 更灵活,适用于动态返回闭包或长期存储。
  • 在 Rust 2021+ 版本中,尽量优先使用 impl Fn,除非必须动态分发。

4.高阶函数技巧

函数组合
rust 复制代码
fn main() {
    let add_one = |x| x + 1;
    let square = |x| x * x;
    let add_then_square = compose(add_one, square);
    assert_eq!(add_then_square(2), 9);
}

fn compose<F,G,A,B,C>(f:F,g:G) -> impl Fn(A) -> C
where
    F:Fn(A) -> B,
    G:Fn(B) -> C,
{
    move |x| g(f(x))
}

5.闭包与迭代器高级用法

自定义迭代器适配器
rust 复制代码
fn main() {
    let nums = vec![1, 2, 3, 4, 5];
    let evens: Vec<_> = nums.into_iter().my_filter(|&x| x % 2 == 0).collect();
    assert_eq!(evens, vec![2, 4]);
}
trait MyIterExt: Iterator{
    fn my_filter<F>(self,f:F) -> MyFilter<Self,F>
    where
        F: FnMut(&Self::Item) -> bool,
        Self:Sized,
    {
        MyFilter { iter: self, f }
    }
}
struct MyFilter<I,F>{
    iter:I,
    f:F,
}

impl <I:Iterator,F> Iterator for MyFilter<I,F>
where
    F: FnMut(&I::Item) -> bool,
{
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        while let Some (item) = self.iter.next() {
            if (self.f)(&item) {
                return Some(item);
            }
        }
        None
    }

}
impl <T: ?Sized> MyIterExt for T where T: Iterator{}

示例解释

1.main函数

rust 复制代码
fn main() {
    let nums = vec![1, 2, 3, 4, 5];
    let evens: Vec<_> = nums.into_iter().my_filter(|&x| x % 2 == 0).collect();
    assert_eq!(evens, vec![2, 4]);
}
  • 创建一个 Vec<i32>,包含 [1, 2, 3, 4, 5]

  • 调用 into_iter() 将其转换为迭代器。

  • 使用自定义的 my_filter 方法过滤出偶数(x % 2 == 0)。

  • 最终 collect() 收集结果,并断言 evens == [2, 4]

2.MyIterExt Trait(扩展方法)

rust 复制代码
trait MyIterExt: Iterator {
    fn my_filter<F>(self, f: F) -> MyFilter<Self, F>
    where
        F: FnMut(&Self::Item) -> bool,
        Self: Sized,
    {
        MyFilter { iter: self, f }
    }
}
  • MyIterExt 是一个 trait ,为所有实现了 Iterator 的类型(如 Vec<T> 的迭代器)添加 my_filter 方法。
  • my_filter
    • 接收一个闭包 f: F,用于过滤元素(FnMut(&Item) -> bool)。
    • 返回 MyFilter 结构体(一个自定义迭代器)。
  • Self: Sized
    • 限制 Self 必须是编译期已知大小的类型(因为 my_filter 要按值返回 MyFilter)。

3.MyFilter 结构体(自定义迭代器)

rust 复制代码
struct MyFilter<I, F> {
    iter: I,  // 底层迭代器
    f: F,     // 过滤条件(闭包)
}
  • 这是一个泛型结构体:
    • I 是底层迭代器(如 std::vec::IntoIter<i32>)。
    • F 是闭包类型(如 |&x| x % 2 == 0)。

4.为 MyFilter 实现 Iterator

rust 复制代码
impl<I: Iterator, F> Iterator for MyFilter<I, F>
where
    F: FnMut(&I::Item) -> bool,  // 闭包接受 `&Item` 并返回 `bool`
{
    type Item = I::Item;  // 输出类型和底层迭代器一致

    fn next(&mut self) -> Option<Self::Item> {
        while let Some(item) = self.iter.next() {
            if (self.f)(&item) {  // 调用闭包检查是否保留该元素
                return Some(item); // 如果条件满足,返回该元素
            }
        }
        None  // 迭代结束
    }
}
  • next 方法
    • 不断从底层迭代器 self.iter 获取下一个元素。
    • 用闭包 self.f 检查该元素是否满足条件。
    • 如果满足,返回 Some(item);否则继续循环。
    • 如果底层迭代器耗尽,返回 None

5.为所有 Iterator 实现 MyIterExt

rust 复制代码
impl<T: ?Sized> MyIterExt for T where T: Iterator {}
  • ?Sized 表示 T 可以是动态大小类型(DST),但这里 Iterator 本身要求 Self: Sized,所以实际上 T 必须是 Sized
  • 这个实现让 所有 Iterator 类型 自动获得 my_filter 方法。

6.与标准库 filter 的区别

特性 标准库 filter 这里的 my_filter
闭包参数 FnMut(&Self::Item) -> bool 相同
返回类型 Filter<Self, F> MyFilter<Self, F>
实现方式 标准库内置 自定义
适用场景 通用 学习/自定义扩展

6.闭包与生命周期

闭包中的生命周期注解
rust 复制代码
fn main() {
    let processor = {
        let text = String::from("hello"); // `text` 在这个块中创建
        make_processor(&text)             // 返回的闭包捕获了 `&text`
    }; // `text` 在这里被销毁!
    // 但 `processor` 还试图持有 `text` 的引用 → 悬垂指针!
}

fn make_processor<'a>(s: &'a str) -> Box<dyn Fn() -> &'a str + 'a> {
    Box::new(move || s) // 闭包捕获 `s`(即 `&text`)
}
高阶生命周期模式
rust 复制代码
fn main() {
    // 定义一个包含1, 2, 3的Vec<i32>
    let nums = vec![1, 2, 3];
    
    // 调用apply_to_all函数,传入nums的引用和一个计算平方的闭包
    // |x| x * x 是一个闭包,接收一个i32引用,返回它的平方值
    let squared = apply_to_all(&nums, |x| x * x);
    
    // 断言结果是否等于预期的vec![1, 4, 9]
    assert_eq!(squared, vec![1, 4, 9]);
}

/// 对切片中的每个元素应用给定的闭包,返回处理后的结果集合
///
/// # 参数
/// * `items` - 一个 `i32` 类型的切片引用,生命周期标记为 `'a`
/// * `f` - 一个闭包,接受 `&'a i32` 并返回 `i32`
///
/// # 返回值
/// 返回一个新的 `Vec<i32>`,包含对每个元素应用闭包后的结果
///
/// # 泛型约束
/// * `F` - 必须实现 `FnMut` trait,表示可以可变地捕获环境
/// * 闭包参数和返回值的生命周期与输入切片一致 (`'a`)
fn apply_to_all<'a, F>(items: &'a [i32], f: F) -> Vec<i32>
where
    F: FnMut(&'a i32) -> i32,  // F是一个闭包,接受&i32返回i32
{
    // 使用迭代器处理:
    // 1. items.iter() - 创建切片的迭代器
    // 2. .map(f) - 对每个元素应用闭包f
    // 3. .collect() - 将结果收集到Vec中
    items.iter().map(f).collect()
}

7.闭包与多线程

跨线程闭包
rust 复制代码
use std::thread;  // 导入标准库的线程模块

fn main() {
    let data = vec![1, 2, 3];  // 创建一个 Vec<i32>

    // 创建一个新线程
    thread::spawn(move || {     // `move` 关键字强制闭包获取 `data` 的所有权
        println!("Data in thread: {:?}", data);  // 在新线程中打印 data
    })
        .join()   // 等待线程结束
        .unwrap(); // 处理可能的错误
}
线程池中的闭包
rust 复制代码
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    for i in 0..5 {
        let tx = tx.clone();
        thread::spawn(move || {
            tx.send(i * 2).unwrap();
        });
    }

    drop(tx); // 关键!关闭主线程的发送端

    let results: Vec<_> = rx.iter().collect(); 
    println!("{:?}", results); // 正常输出 [0, 2, 4, 6, 8] 顺序会有变动
}

8.闭包与模式匹配

闭包中的解构
rust 复制代码
fn main() {
    let pairs = vec![(1, "one"), (2, "two"), (3, "three")];
    let strings: Vec<_> = pairs.into_iter().map(|(num,word)| format!("{}: {}", num, word)).collect();
    assert_eq!(strings, vec!["1: one", "2: two", "3: three"]);
}
复杂模式匹配
rust 复制代码
fn main() {
    let points = vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4 }];
    let x_coords: Vec<_> = points.into_iter().map(|Point { x, .. }| x).collect();
    assert_eq!(x_coords, vec![1, 3]);
}

struct Point {
    x: i32,
    y: i32,
}

9.闭包与错误处理

闭包中的 Result 处理
rust 复制代码
fn main() {
    let result = try_operation(|| {
        if true {
            Ok(42)
        } else {
            Err("failed".to_string())
        }
    });
    assert_eq!(result, Ok(84));
}

fn try_operation<F>(op: F) -> Result<i32, String>
where
    F: FnOnce() -> Result<i32, String>,
{
    op().map(|x| x * 2)
}
使用 ? 运算符
rust 复制代码
fn main() -> Result<(), String>{
    let nums = vec![1, 2, 3];
    let processed = process_data(&nums, |x| {
        if *x > 0 {
            Ok(x * 2)
        } else {
            Err("Negative number".to_string())
        }
    })?;
    println!("{:?}", processed);
    Ok(())
}

fn process_data<F>(data:&[i32],f:F) -> Result<Vec<i32>,String>
where
    F:Fn(&i32) -> Result<i32,String>,
{
    data.iter().map(f).collect()
}

10.闭包与性能优化

避免闭包分配
rust 复制代码
fn main() {
    bad_perf();
    good_perf();
}

fn bad_perf(){
    let mut sum = 0;
    for i in 0..1000{
        let mut add = |x| sum += x;
        add(i);
    }
    println!("sum (bad): {}", sum);
}

fn good_perf(){
    let mut sum = 0;
    let mut add = |x| sum += x;
    for i in 0..1000 {
        add(i);
    }
    println!("sum (good): {}", sum);
}
内联闭包
rust 复制代码
// 编译器通常会内联简单的闭包
let nums = vec![1, 2, 3];
let sum: i32 = nums.iter().map(|x| x * 2).sum();

Rust 编译器(特别是 LLVM 优化器 )会对简单的闭包(如 |x| x * 2)进行 内联优化(Inline Optimization)

  1. 内联 :编译器会将闭包的逻辑直接展开到调用处,而不是生成一个独立的函数调用。
    • 例如,map(|x| x * 2) 可能会被优化成类似 for x in nums { x * 2 } 的循环。
  2. 零成本抽象 :迭代器和闭包在 Rust 中是零成本的,编译后的代码和手写的 for 循环性能几乎相同。
  3. 性能优势:避免了闭包的运行时开销(如虚函数调用)。

5.总结

Rust 的闭包是一个强大而灵活的特性,它:

  1. 允许简洁的匿名函数语法
  2. 可以捕获环境变量
  3. 通过 trait 系统 (Fn, FnMut, FnOnce) 提供灵活的调用语义
  4. 在迭代器、并发、回调等场景中有广泛应用
  5. 几乎零运行时开销,因为大多数情况下会被内联

理解闭包的工作原理和限制对于编写高效、优雅的 Rust 代码至关重要。通过合理使用闭包,可以大大简化代码并提高表达能力。

相关推荐
martinzh2 小时前
Spring AI 项目介绍
后端
susnm2 小时前
Dioxus 与数据库协作
前端·rust
前端付豪2 小时前
20、用 Python + API 打造终端天气预报工具(支持城市查询、天气图标、美化输出🧊
后端·python
爱学习的小学渣2 小时前
关系型数据库
后端
武子康2 小时前
大数据-33 HBase 整体架构 HMaster HRegion
大数据·后端·hbase
前端付豪2 小时前
19、用 Python + OpenAI 构建一个命令行 AI 问答助手
后端·python
凌览2 小时前
斩获 27k Star,一款开源的网站统计工具
前端·javascript·后端
全栈凯哥2 小时前
02.SpringBoot常用Utils工具类详解
java·spring boot·后端
狂师2 小时前
啥是AI Agent!2025年值得推荐入坑AI Agent的五大工具框架!(新手科普篇)
人工智能·后端·程序员
羊八井2 小时前
类型、分类定义时使用 type 还是 kind ?
rust·typescript·代码规范