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 中的闭包。如果你有任何问题或想进一步探讨,欢迎留言交流!

相关推荐
Hello.Reader6 分钟前
在 Web 中调试 Rust-Generated WebAssembly
前端·rust·wasm
疏狂难除1 小时前
Windows安装Rust版本GDAL
rust·gdal
讲究事3 小时前
Built-in functions
rust·gpu
苏近之3 小时前
我用 Rust 写了一个 Hello World
rust
悟空非空也4 小时前
太炸裂,10分钟小白用Cursor开发自己的MCP服务器,赶紧学起来
ai编程·cursor·mcp
Aibo0076 小时前
MCP 实战:从工具入门到企业级应用
ai编程·mcp
Apifox6 小时前
Apifox 全面支持 LLMs.txt:让 AI 更好地理解你的 API 文档
llm·ai编程·cursor
Dlimeng7 小时前
OpenAI发布GPT-4.1系列模型——开发者可免费使用
人工智能·ai·chatgpt·openai·ai编程·agents·gpt-41
Loving_enjoy8 小时前
【用ChatGPT学编程】让AI成为你的编程外脑:注释生成与Debug实战秘籍
chatgpt·ai编程
架构精进之路9 小时前
LangGraph:如何用“图思维”轻松管理多Agent协作?
后端·langchain·ai编程