究竟什么是 Fn、FnMut 和 FnOnce?深入剖析 Rust 闭包

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

Rust 中的闭包:魔法还是严谨的数学?

\n\n

如果您曾经在 Rust 中写过类似 |x| x + 1 的代码,那么您已经使用过闭包了。但是,一旦您尝试将这个闭包传递给函数或将其存储在结构体中,编译器就会突然开始要求:“是哪个 trait?Fn、FnMut 还是 FnOnce?”。

\n\n

这时,许多人会陷入困境。特别是如果您来自 JavaScript、Python 或 Ruby,在这些语言中,闭包只是“捕获变量的函数”。在 Rust 中,情况更复杂(也更有趣)。

\n\n

今天,我们将深入探讨当 Rust 决定为您的闭包赋予哪个 trait 时,底层究竟发生了什么。这不是魔法——这是一个严格的类型系统,可以保护您免受数据竞争和悬垂指针的困扰。我们开始吧!

\n\n

要点概述:三种闭包 trait

\n\n

在 Rust 中,闭包实现了三种 trait:

\n\n
    \n
  • FnOnce——只能被调用一次。它获取捕获变量的所有权。
  • \n
  • FnMut——可以被调用多次,但可以修改捕获的变量(获取 &mut 引用)。
  • \n
  • Fn——可以被调用多次,并且不修改捕获的变量(获取 & 引用)。
  • \n
\n\n

每个闭包会自动实现所有适用的 trait。也就是说,如果一个闭包实现了 Fn,它也会自动实现 FnMut 和 FnOnce。但反之则不成立。

\n\n

为什么?因为 Fn 是最严格的,而 FnOnce 是最宽松的。继承层次结构如下:

\n\n
FnOnce  ←  FnMut  ←  Fn
\n\n

也就是说,FnMut: FnOnce,而 Fn: FnMut。请记住这个阶梯——它至关重要。

\n\n

Rust 如何决定为闭包赋予哪个 trait?

\n\n

编译器会查看闭包具体如何使用捕获的变量。有三种捕获模式:

\n\n
    \n
  • 通过引用 (&T)——闭包只读取变量。
  • \n
  • 通过可变引用 (&mut T)——闭包修改变量。
  • \n
  • 通过值 (T)——闭包获取变量的所有权。
  • \n
\n\n

这如何影响 trait 如下所示:

\n\n\n \n \n \n \n
捕获方式实现的 trait示例
仅 &TFn + FnMut + FnOnce|| println!("{}", x)
&mut T(但不是 T)FnMut + FnOnce|| x += 1
T(所有权)仅 FnOnce|| drop(x) 或 move || x
\n\n

让我们用实际代码来分析每种情况。

\n\n

示例 1:只读 → Fn

\n\n
let x = 42;\nlet read_closure = || println!("x = {}", x);\n\n// 可以任意多次调用\nread_closure();\nread_closure();\n\n// 甚至可以传递给需要 Fn 的函数\nfn call_fn<F: Fn()>(f: F) {\n    f();\n}\ncall_fn(read_closure);
\n\n

这里,闭包只是读取 x。它不需要修改或获取所有权。Rust 明白:“好的,这是安全的——我们赋予它 Fn”。

\n\n

示例 2:修改 → FnMut

\n\n
let mut x = 42;\nlet mut mut_closure = || {\n    x += 1;\n    println!("x = {}", x);\n};\n\nmut_closure(); // x = 43\nmut_closure(); // x = 44\n\n// 传递给需要 FnMut 的函数\nfn call_fn_mut<F: FnMut()>(mut f: F) {\n    f();\n    f();\n}\ncall_fn_mut(mut_closure);
\n\n

请注意:我们将 mut_closure 声明为 let mut。为什么?因为调用 FnMut 需要可变

博客

书籍推荐