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 代码至关重要。通过合理使用闭包,可以大大简化代码并提高表达能力。

相关推荐
秋野酱几秒前
基于javaweb的SpringBoot爱游旅行平台设计和实现(源码+文档+部署讲解)
java·spring boot·后端
小明.杨17 分钟前
Django 中时区的理解
后端·python·django
有梦想的攻城狮20 分钟前
spring中的@Async注解详解
java·后端·spring·异步·async注解
qq_124987075328 分钟前
原生小程序+springboot+vue医院医患纠纷管理系统的设计与开发(程序+论文+讲解+安装+售后)
java·数据库·spring boot·后端·小程序·毕业设计
明月看潮生1 小时前
青少年编程与数学 02-019 Rust 编程基础 10课题、函数、闭包和迭代器
开发语言·青少年编程·rust·编程与数学
lybugproducer1 小时前
浅谈 Redis 数据类型
java·数据库·redis·后端·链表·缓存
明月看潮生1 小时前
青少年编程与数学 02-019 Rust 编程基础 09课题、流程控制
开发语言·算法·青少年编程·rust·编程与数学
焚 城1 小时前
.NET8关于ORM的一次思考
后端·.net
撸猫7913 小时前
HttpSession 的运行原理
前端·后端·cookie·httpsession
嘵奇4 小时前
Spring Boot中HTTP连接池的配置与优化实践
spring boot·后端·http