Rust 闭包:捕获环境的魔法函数

Rust 闭包:捕获环境的魔法函数

今天来聊聊 Rust 语言中一个非常强大且有趣的特性:闭包 (Closures)。你可能在其他编程语言听说过闭包这个词,简单来说,闭包就像一个"加强版"的函数,不仅能执行代码,还能"记住"并访问定义时所处的环境中的变量。

更多 Rust 闭包视频

什么是闭包?

通常我们所说的闭包,在计算机科学中指的是词法闭包 (Lexical Closure)。你可以把它想象成一个函数,这个函数自带了一个"小背包",里面装着出生时(定义时)那个环境里的一些东西,特别是那些在函数内部没有定义但被用到的变量。

  • 外部环境:这就是闭包被定义时所处的作用域。
  • 自由变量:在函数内部使用,但既不是函数的参数也不是在函数内部定义的变量,就叫做自由变量。在函数式编程中,我们也会听到这个术语。所以闭包可以是自备装备参与战斗。

核心概念: 闭包就是将自由变量和自身"绑定"在一起的函数。当闭包被创建时,它会捕获(capture)它周围环境中的变量,这样即使在定义闭包的环境结束后,这个闭包仍然可以访问和操作这些被捕获的变量。

rust 复制代码
fn counter(i: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |n: i32| n + i)
}

fn main(){
    let f = counter(2);
    assert_eq!(3, f(1));
}

在这个例子中,counter 函数返回了一个闭包。这个闭包是 |n: i32| n + i。注意,这个闭包用到了变量 i,但 i 并不是闭包的参数,也不是在闭包内部定义的。i 就是一个自由变量,它来自 counter 函数的作用域。当我们在 main 函数中调用 counter(2) 时,闭包就捕获了当时 i 的值 2。之后,当我们调用 f(1) 时,实际上执行的是 1 + 2,所以结果是 3

闭包的特点:不仅仅是函数

闭包之所以强大,是因为它具备以下这些特性:

  • 加强版的函数: 没错,闭包本质上仍然是函数,很多时候我们用匿名函数的形式来创建闭包。
  • Rust 的语法糖: 在 Rust 中,闭包的语法设计得非常简洁,用 || {} 的形式就能快速创建一个匿名函数。这背后其实是 Rust 编译器帮我们做了很多工作。
  • 捕获环境中的自由变量: 这是闭包最核心的特性。普通函数只能访问传递给它的参数和函数内部定义的变量,而闭包可以访问其定义时环境中的变量。
  • 绑定自由变量: 正如我们之前所说,闭包会将它用到的自由变量和自身绑定在一起。
  • 可以添加变量: 虽然原文没有展开,但闭包本身也可以拥有自己的局部变量。
  • 作为参数或返回值: 闭包可以像普通函数一样,作为其他函数的参数传递,也可以作为函数的返回值。这使得我们可以编写更加灵活和高阶的函数。
  • 引用环境中的变量方式灵活: 闭包如何引用其环境中的变量取决于变量在闭包内的使用方式,这涉及到所有权和借用的概念,我们稍后会详细讨论。
  • 不是函数指针而是 trait: 这是一个很重要的技术细节。在 Rust 中,闭包并不是像 C 语言那样的简单函数指针。每个闭包都有其独特的类型,它们实现了 FnFnMutFnOnce 这三个 trait 之一,这决定了闭包如何捕获和使用环境中的变量。

闭包的基本语法

Rust 中闭包的基本语法是这样的:

rust 复制代码
|parameters| -> return_type {
    // 闭包体
}
  • parameters: 闭包接收的参数列表,可以为空。参数类型通常可以由编译器自动推断。
  • return_type : 闭包的返回值类型,通常也可以由编译器自动推断。如果闭包体只有一个表达式,{} 可以省略。
  • {}: 闭包体,包含要执行的代码。

例如:

rust 复制代码
let add_one = |x: i32| x + 1;
let print_hello = || println!("Hello!");
let multiply = |a, b| a * b; // 参数类型和返回值类型可以省略,由编译器推断

闭包的参数可以是任意类型

这和普通函数一样,闭包的参数可以是 Rust 支持的任何数据类型。

相同定义(同构)的闭包却属于不同类型

文章中提到了一个有趣的现象:

rust 复制代码
fn main(){
    let c1 = || {};
    let c2 = || {};
    let _v = [c1, c2]; // 报错!
}

这里会报错,因为 c1c2 虽然看起来定义相同(都是没有参数也没有返回值的闭包),但它们实际上是不同的类型。每个闭包实例都有其自己独特的匿名类型。这与泛型不同,即使两个闭包的行为完全一样,它们的类型也是不一样的。

模拟编译器对闭包的实现

文章中给出了一个模拟编译器如何实现闭包的例子:

rust 复制代码
#![feature(unboxed_closures)]
#![feature(fn_traits)]

struct Adder {
    a: u32
}

impl FnOnce<(u32, )> for Adder {
    type Output = u32;
    extern "rust-call" fn call_once(self, b: (u32, )) -> Self::Output {
        self.a + b.0
    }
}

fn main() {
    let adder = Adder { a: 3 };
    assert_eq!(adder(2), 5);
}

这个例子通过自定义一个结构体 Adder 并为其实现 FnOnce trait,来模拟一个捕获了外部变量 a 的闭包。当调用 adder(2) 时,实际上是调用了 call_once 方法,这个方法使用了 Adder 结构体中存储的 a 值和传入的参数 b.0 进行计算。

SUGGESTION: 可以更详细地解释 FnOnce(u32, ) 这个元组参数、extern "rust-call" 调用约定以及 self 的含义,帮助读者理解闭包在底层是如何通过结构体和 trait 来实现的。

非装箱 (Non-Boxing)

Rust 闭包的一个重要特点是非装箱。这意味着:

  • 更好的控制优化: 编译器可以更直接地操作闭包捕获的变量,避免了额外的堆分配和间接访问,从而实现更好的性能优化。
  • 支持按值和按引用绑定环境变量: 闭包可以灵活地选择如何捕获其环境中的变量,可以是直接复制值(move),也可以是借用引用(borrow)。
  • 支持 3 种不同的闭包访问 self&self&mut self 这与 Rust 的所有权和借用规则紧密相关,对应于 FnOnce(消耗所有权)、Fn(不可变借用)和 FnMut(可变借用)这三个闭包 trait。

Fn 闭包:可以多次调用

实现了 Fn trait 的闭包可以对其捕获的环境进行不可变借用 (&self)。这意味着闭包在执行时不会修改它捕获的变量,因此可以安全地被多次调用。

rust 复制代码
fn main(){
    let greeting = String::from("Hey!");

    let fn_closure = || {
        println!("Closure says: {}", greeting);
    };

    fn_closure();
    fn_closure(); // 可以多次调用

    println!("{}", greeting); // greeting 的所有权仍然在 main 函数中
}

在这个例子中,fn_closure 借用了 greeting 字符串的引用,所以它可以多次打印 greeting 的值,而不会影响 greeting 本身的所有权。

FnMut 闭包:可以修改捕获的变量

实现了 FnMut trait 的闭包可以对其捕获的环境进行可变借用 (&mut self)。这意味着闭包在执行时可以修改它捕获的变量。

rust 复制代码
fn main(){
    let mut counter = 0;

    let mut fn_mut_closure = || {
        counter += 1;
        println!("Counter is now: {}", counter);
    };

    fn_mut_closure(); // Counter is now: 1
    fn_mut_closure(); // Counter is now: 2

    println!("Final counter: {}", counter); // Final counter: 2
}

这里,fn_mut_closure 可变地借用了 counter 变量,每次调用闭包都会修改 counter 的值。注意闭包本身需要声明为 mut,因为它会修改其捕获的环境。

FnOnce 闭包:只能调用一次

实现了 FnOnce trait 的闭包会获取其捕获变量的所有权 ,或者将捕获的变量移动 (move) 到闭包内部 。这意味着闭包在执行后,其捕获的变量可能不再有效,因此 FnOnce 闭包只能被调用一次。

rust 复制代码
fn main(){
    let a = Box::new(23);
    let call_me = || {
        let c = a; // 将 a 的所有权移动到闭包内部
        println!("Value: {}", c);
        // a 在这里已经失效
    };
    call_me();
    // call_me(); // 再次调用会报错,因为闭包已经消耗了 a 的所有权
}

在这个例子中,闭包 call_me 获取了 a 的所有权。一旦 call_me() 被调用,a 的所有权就转移到了闭包内部的 c 变量,外部的 a 变得无效。因此,我们不能再次调用 call_me()

使用 move 关键字自动实现 FnOnce

如果你希望闭包获取其环境中所有变量的所有权,可以使用 move 关键字:

rust 复制代码
fn main() {
    let name = String::from("Alice");

    let move_closure = move || {
        println!("Name: {}", name);
        // name 的所有权已经移动到闭包内部
    };

    move_closure();
    // println!("{}", name); // 报错!name 的所有权已经转移
}

使用 move 关键字创建的闭包通常会实现 FnOnce trait,因为变量的所有权被移动到了闭包内部。

作为参数

闭包可以作为函数的参数传递,这使得我们可以编写更加灵活和通用的函数。通常我们会使用泛型和 trait bounds 来接受闭包作为参数:

rust 复制代码
fn call_me<F: Fn()>(f: F) {
    f();
}

fn function() {
    println!("I'm a function!");
}

fn main() {
    let closure = || println!("I'm a closure!");

    call_me(closure); // 传递闭包
    call_me(function); // 也可以传递普通函数,因为函数也实现了 Fn trait
}

在这个例子中,call_me 函数接受一个实现了 Fn() trait 的参数 f。这意味着任何没有参数也没有返回值的闭包(或者函数)都可以传递给 call_me

作为返回值

闭包也可以作为函数的返回值。由于闭包有其独特的匿名类型,直接返回一个具体的闭包类型是不可行的。通常我们会使用 impl Trait 语法来返回一个实现了特定闭包 trait 的类型:

rust 复制代码
fn create_fn() -> impl Fn() {
    let text = "Fn".to_owned();
    move || println!("This is a: {}", text)
}

fn create_fnmut() -> impl FnMut() {
    let mut text = "FnMut".to_owned();
    move || text.push_str(" - Modified");
}

fn create_fnonce() -> impl FnOnce() {
    let text = "FnOnce".to_owned();
    move || println!("This is a: {}", text)
}

fn main() {
    let fn_plain = create_fn();
    fn_plain(); // This is a: Fn

    let mut fn_mut = create_fnmut();
    fn_mut();
    fn_mut();

    let fn_once = create_fnonce();
    fn_once();
    // fn_once(); // 再次调用会报错
}

在这个例子中,create_fn 返回一个实现了 Fn() trait 的闭包,create_fnmut 返回一个实现了 FnMut() trait 的闭包,create_fnonce 返回一个实现了 FnOnce() trait 的闭包。注意我们使用了 move 关键字将 text 的所有权移动到闭包内部,这样即使 create_fn 函数结束,闭包仍然可以访问 text

更多 Rust 闭包视频

总结

闭包是 Rust 中一个非常强大且灵活的特性,通过闭包我们创建可以捕获和操作其定义环境中变量的匿名函数。理解 FnFnMutFnOnce 这三个闭包 trait,以及闭包如何捕获变量(通过引用或移动所有权),对于编写高效且符合 Rust 所有权和借用规则的代码至关重要。掌握闭包,你就能写出更加简洁、优雅且富有表达力的 Rust 代码!

希望这篇文章能够帮助你更好地理解 Rust 中的闭包。如果你有任何问题或想进一步探讨,欢迎留言交流!

相关推荐
一眼万年0412 分钟前
每天都在使用的VS Code Copilot Chat 开源啦!
aigc·ai编程·visual studio code
饼干哥哥15 分钟前
AI编程搞钱|从0到1,用Cursor开发浏览器插件,上架谷歌商城赚美金
ai编程
寻月隐君32 分钟前
告别竞态条件:基于 Axum 和 Serde 的 Rust 并发状态管理最佳实践
后端·rust·github
鬼鬼鬼9 小时前
从软件1.0到3.0:在这场AI浪潮中,我们如何面对?
aigc·ai编程·cursor
散步去海边9 小时前
Cursor 进阶使用教程
前端·ai编程·cursor
摆烂工程师9 小时前
国内如何安装和使用 Claude Code 教程 - Windows 用户篇
人工智能·ai编程·claude
成遇9 天前
在Vscode中安装Sass并配置
vscode·rust·sass
极客密码9 天前
Cursor再见!简单两步,Augment真无限续杯,爽用Claude 4!
ai编程·cursor·trae
止观止9 天前
Rust智能指针演进:从堆分配到零复制的内存管理艺术
开发语言·后端·rust
学無芷境9 天前
Cargo 与 Rust 项目
开发语言·后端·rust