Rust 如何处理闭包:Fn、FnMut 和 FnOnce

在 Rust 编程语言中,闭包是一种强大的特性,允许你定义匿名函数并捕获周围环境中的变量。Rust 的闭包系统由三个核心特征定义------Fn、FnMut 和 FnOnce。理解这些特征对于掌握 Rust 的闭包机制以及编写高效、安全的代码至关重要。

本文将详细介绍这三个特征,包括它们的定义、用例、适用场景和最佳实践,并提供代码示例,帮助你全面学习相关概念。

什么是 Fn、FnMut 和 FnOnce?

Fn、FnMut 和 FnOnce 是 Rust 标准库中定义的三个特征,用于描述闭包(或任何可调用对象)的行为。它们的主要区别在于如何访问捕获的变量以及在被调用时的所有权规则:

  • FnOnce:表示闭包可以被调用一次。被调用后,闭包本身会被消耗,不能再被使用。
  • FnMut:表示闭包可以被多次调用,并且在被调用时可以修改捕获的变量。
  • Fn:表示闭包可以被多次调用,并且只读取捕获的变量而不修改它们。

这三个特征之间存在继承关系:

  • Fn 继承自 FnMut,而 FnMut 继承自 FnOnce。
  • 因此,如果一个闭包实现了 Fn,它也会自动实现 FnMut 和 FnOnce;如果一个闭包实现了 FnMut,它也会实现 FnOnce。

每个特征的定义

FnOnce

FnOnce 特征定义了一个 call_once 方法,其签名如下:

rust 复制代码
pub trait FnOnce<Args> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}
  • 特性:call_once 接收 self(而不是 &self 或 &mut self),这意味着当闭包被调用时,它会转移其所有权,因此只能被调用一次。
  • 用例:适用于闭包需要移动捕获的变量或执行一次性操作的场景。

FnMut

FnMut 特征定义了一个 call_mut 方法,其签名如下:

rust 复制代码
pub trait FnMut<Args>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
  • 特性:call_mut 接收 &mut self,允许闭包在被调用时修改其内部状态或捕获的变量,并且可以被多次调用。
  • 用例:适用于闭包需要在多次调用中修改其环境的场景。

Fn

Fn 特征定义了一个 call 方法,其签名如下:

rust 复制代码
pub trait Fn<Args>: FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}
  • 特性:call 接收 &self,这意味着闭包借用不可变。它可以被多次调用而不修改环境。
  • 用例:适用于闭包需要被多次调用且只读取数据的场景。

闭包如何实现这些特征

Rust 编译器会根据闭包如何使用捕获的变量自动确定它实现了哪些特征。闭包可以以三种方式捕获变量:

  • 按值(move) :闭包获取变量的所有权。
  • 按可变引用(&mut) :闭包捕获变量的可变引用。
  • 按不可变引用(&) :闭包捕获变量的不可变引用。

实现的特征取决于捕获变量的使用方式:

  • 仅实现 FnOnce:闭包移动了捕获的变量。
  • 实现 FnMut 和 FnOnce:闭包修改了捕获的变量。
  • 实现 Fn、FnMut 和 FnOnce:闭包只读取捕获的变量。

代码示例

实现 FnOnce 的闭包

rust 复制代码
fn main() {
    let s = String::from("hello");
    let closure = move || {
        drop(s); // 移动 s 并释放它
    };
    closure(); // 被调用一次
    // closure(); // 错误:闭包已被消耗
}

解释:闭包通过 move 捕获 s,获取其所有权并在被调用时释放它。由于 s 被移动,闭包只能被调用一次,因此它仅实现 FnOnce。

实现 FnMut 的闭包

rust 复制代码
fn main() {
    let mut s = String::from("hello");
    let mut closure = || {
        s.push_str(" world"); // 修改 s
    };
    closure(); // 第一次调用
    closure(); // 第二次调用
    println!("{}", s); // 输出 "hello world world"
}

解释:闭包通过可变引用捕获 s 并在每次调用时修改它。由于它需要修改环境,因此它实现了 FnMut 和 FnOnce。

实现 Fn 的闭包

rust 复制代码
fn main() {
    let s = String::from("hello");
    let closure = || {
        println!("{}", s); // 读取 s 而不修改
    };
    closure(); // 第一次调用
    closure(); // 第二次调用
}

解释:闭包通过不可变引用捕获 s 并只读取它而不修改。因此,它实现了 Fn、FnMut 和 FnOnce。

在函数参数中使用这些特征

闭包可以作为参数传递给函数,函数需要使用特征约束来指定所需的闭包行为。

使用 FnOnce

rust 复制代码
fn call_once<F>(f: F)
where
    F: FnOnce(),
{
    f();
}

fn main() {
    let s = String::from("hello");
    call_once(move || {
        drop(s);
    });
}

解释:call_once 接受一个 FnOnce 闭包并调用它一次,适用于移动捕获变量的闭包。

使用 FnMut

rust 复制代码
fn call_mut<F>(mut f: F)
where
    F: FnMut(),
{
    f();
    f();
}

fn main() {
    let mut s = String::from("hello");
    call_mut(|| {
        s.push_str(" world");
    });
    println!("{}", s); // 输出 "hello world world"
}

解释:call_mut 接受一个 FnMut 闭包并调用它两次。闭包可以修改捕获的变量。注意 f 必须被声明为 mut。

使用 Fn

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

fn main() {
    let s = String::from("hello");
    call_fn(|| {
        println!("{}", s);
    });
}

解释:call_fn 接受一个 Fn 闭包并调用它两次。闭包只读取捕获的变量。

何时使用每个特征?

选择正确的特征取决于闭包所需的行为:

最佳实践

  • 优先使用 Fn:如果闭包不需要修改变量,使用 Fn 以获得最大兼容性。
  • 在需要修改时使用 FnMut:当闭包需要更新状态时选择 FnMut。
  • 在单次使用时使用 FnOnce:如果闭包移动变量或执行一次性任务,使用 FnOnce。
  • 为 API 选择正确的特征:对于单次调用使用 FnOnce,对于多次只读调用使用 Fn,对于多次调用并修改使用 FnMut。
  • 注意生命周期:确保捕获的变量存活时间足够长,以避免借用错误。

原文:dev.to/leapcell/ho...

相关推荐
红尘散仙20 分钟前
七、WebGPU 基础入门——Texture 纹理
前端·rust·gpu
红尘散仙21 分钟前
八、WebGPU 基础入门——加载图像纹理
前端·rust·gpu
w4ngzhen26 分钟前
关于Bevy中的原型Archetypes
rust·游戏开发
sayornottt2 小时前
Rust中的动态分发
后端·rust
YiSLWLL11 小时前
使用Tauri 2.3.1+Leptos 0.7.8开发桌面小程序汇总
python·rust·sqlite·matplotlib·visual studio code
yu41062111 小时前
Rust 语言使用场景分析
开发语言·后端·rust
朝阳58114 小时前
Rust项目GPG签名配置指南
开发语言·后端·rust
朝阳58114 小时前
Rust实现高性能目录扫描工具ll的技术解析
开发语言·后端·rust
红尘散仙19 小时前
六、WebGPU 基础入门——Vertex 缓冲区和 Index 缓冲区
前端·rust·gpu
苏近之19 小时前
深入浅出 Rust 异步运行时原理
rust·源码