Rust 中的闭包:魔法还是严谨的数学?
\n\n如果您曾经在 Rust 中写过类似 |x| x + 1 的代码,那么您已经使用过闭包了。但是,一旦您尝试将这个闭包传递给函数或将其存储在结构体中,编译器就会突然开始要求:“是哪个 trait?Fn、FnMut 还是 FnOnce?”。
这时,许多人会陷入困境。特别是如果您来自 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
每个闭包会自动实现所有适用的 trait。也就是说,如果一个闭包实现了 Fn,它也会自动实现 FnMut 和 FnOnce。但反之则不成立。
为什么?因为 Fn 是最严格的,而 FnOnce 是最宽松的。继承层次结构如下:
FnOnce ← FnMut ← Fn\n\n也就是说,FnMut: FnOnce,而 Fn: FnMut。请记住这个阶梯——它至关重要。
Rust 如何决定为闭包赋予哪个 trait?
\n\n编译器会查看闭包具体如何使用捕获的变量。有三种捕获模式:
\n\n- \n
- 通过引用 (
&T)——闭包只读取变量。 \n - 通过可变引用 (
&mut T)——闭包修改变量。 \n - 通过值 (
T)——闭包获取变量的所有权。 \n
这如何影响 trait 如下所示:
\n\n| 捕获方式 | 实现的 trait | 示例 |
|---|---|---|
仅 &T | Fn + FnMut + FnOnce | || println!("{}", x) |
&mut T(但不是 T) | FnMut + FnOnce | || x += 1 |
T(所有权) | 仅 FnOnce | || drop(x) 或 move || x |
让我们用实际代码来分析每种情况。
\n\n示例 1:只读 → Fn
\n\nlet 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”。
示例 2:修改 → FnMut
\n\nlet 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 需要可变