深入浅出理解Rust闭包

138 阅读4分钟

小贼猫 - 娜美

闭包基本语法

|参数1, 参数2, ...| -> 返回类型 {
    // 闭包体
}

闭包主要特点

参数列表

  • 用竖线 | 包裹
  • 可以省略类型,由编译器推断
  • 如果没有参数,可以写成 ||

返回类型

  • 通常可以省略,由编译器推断
  • 如果需要明确指定,使用 -> 后跟类型

闭包体

  • 如果只有一个表达式,可以省略花括号 {}
  • 多个语句需要用花括号包围

闭包的特性和使用场景

捕获环境变量

闭包可以捕获其定义环境中的变量

使用场景

  • 当需要在函数内部创建一个使用局部变量的函数时
  • 在异步编程中,将上下文传递给未来执行的代码
let factor = 2;
let multiply = |x| x * factor;

灵活的类型推断

闭包参数和返回值的类型通常可以被编译器自动推断

使用场景

  • 编写简洁的代码,特别是函数式编程风格中
  • 使用迭代器方法,如 map, filter
let numbers = vec![1, 2, 3, 4, 5];
let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();
println!("{:?}", squares);

作为函数参数

闭包可以作为函数的参数传递

使用场景

  • 实现回调函数
  • 自定义排序或过滤逻辑
fn apply_operation<F>(x: i32, f: F) -> i32
where
    F: Fn(i32) -> i32,
{
    f(x)
}

let double = |x| x * 2;
println!("结果:{}", apply_operation(5, double));

实现 FnFnMutFnOnce trait

根据如何捕获和使用环境变量,闭包会自动实现这些 trait

使用场景

  • Fn: 多次调用,不能修改捕获的变量
  • FnMut: 多次调用,可能修改捕获的变量
  • FnOnce:只能调用一次,可能消耗捕获的变量
let mut count = 0;
let mut increment = || {
    count += 1;
    println!("计数: {}", count);
};

increment();
increment();

延迟执行

闭包定义代码块,但不立即执行

使用场景

  • 惰性求值
  • 定义可重用的操作
let expensive_calculation = |x| {
    println!("复杂计算...");
    x * x * x
};

let result = expensive_calculation(4);
println!("结果:{:?}", result);

场景小结

闭包在Rust中非常强大和灵活,特别适用于:

  • 函数式编程
  • 自定义迭代器操作
  • 异步编程
  • 事件处理和回调
  • 延迟计算
  • 性能优化

Rust闭包设计目标

Rust中闭包的设计目标是要快:比函数指针还要快,快到甚至可以在对性能敏感的热点代码中使用它们

在大多数语言中,闭包会在堆中分配内存、进行动态派发以及进行垃圾回收。因此,创建、调用和收集每一个闭包都会花费一点点额外的 CPU 时间。更糟的是,闭包往往难以内联,而内联是编译器用来消除函数调用开销并实施大量其他优化的关键技术。总而言之,闭包在这些语言中确实慢到值得手动将它们从节奏紧凑的内层循环中去掉

Rust 闭包则没有这些性能缺陷。它们没有垃圾回收。与 Rust 中的其他所有类型一样,除非你将闭包放在 BoxVec 或其他容器中,否则它们不会被分配到堆上。由于每个闭包都有不同的类型,因此 Rust 编译器只要知道正在调用的闭包的类型,就可以内联该闭包的代码

Rust 的“激进赌注”是基于“必然存在好的替代设计”这个假设的。有时你可以通过让每个闭包接受它需要的引用作为参数,来解决闭包所有权和生命周期的问题。有时你可以为系统中的每个事物分配一个编号,并传递这些编号而不是传递引用。或者你可以实现 MVC 的众多变体之一,其中的对象并非都相互引用。或者将工具包建模为具有单向数据流的非 MVC 系统,比如 Facebook 的 Flux 架构

合理使用闭包,每个人都可以写出更简洁优雅的代码

欢迎大家讨论交流,如果喜欢本文章或感觉文章有用,动动你那发财的小手点赞、收藏、关注再走呗 ^_^ 

微信公众号:草帽Lufei