
rust
Rust 入门丨02 闭包
本文通过"承诺"的视角深入解析 Rust 闭包中 Fn、FnMut、FnOnce 三个 trait 的包含关系,帮助读者理解闭包设计的本质。
笔者之前一直不理解 Rust 中关于闭包的 Fn/FnMut/FnOnce 这 3 个 trait 的包含关系。通过一段时间的学习和分析,终于找到了我思维上的一个错误点 ,特此梳理此文,方便日后查询。
我之前的理解是这样的:
Fn 只需要引用,所以要求是最容易满足的。FnMut 需要的是可变引用,所以能满足 FnMut,一定能满足 Fn。FnOnce 需要的是所有权,那都有所有权了 ,&mut 和 & 肯定就不在话下了。所以满足 FnOnce 的,一定是 Fn 和 FnMut。满足 FnMut 的,不一定是 FnOnce,但是一定是 Fn。
我的错误点在于:在闭包实现者的角度想"我拥有什么权限"。
正确的思路应该是:站在函数调用者的角度想"我得到了什么承诺"。这才是 Trait 设计的本质,即是能力的声明,更是限制的承诺。
核心关键:承诺越强,限制越多,类型越“小”
Fn, FnMut, FnOnce 这三个 Trait,本质上是闭包对调用者做出的三种不同强度的承诺。
Fn的承诺 (最强的承诺)- 承诺内容:调用我,只需要对我进行不可变借用 (
&self)。你甚至可以同时在多个线程里调用我。我保证不会改变任何东西,也不会消耗掉自己。 - 对闭包的限制:为了兑现这个最强的承诺,闭包自身受到的限制也最大。它只能不可变地借用环境中的变量。
- 调用者的自由:调用者获得了最大的自由,可以随心所欲地、不限次数地调用这个闭包。
- 承诺内容:调用我,只需要对我进行不可变借用 (
FnMut的承诺 (中等的承诺)- 承诺内容:调用我,需要对我进行可变借用 (
&mut self)。这意味着你不能同时调用我,但可以一个接一个地调用。我可能会改变我内部的状态。 - 对闭包的限制:限制有所放宽。闭包可以可变地借用环境变量。
- 调用者的自由:调用者的自由受到了一些限制,不能并发调用了。
- 承诺内容:调用我,需要对我进行可变借用 (
FnOnce的承诺 (最弱的承诺)- 承诺内容:你只能调用我一次 (
self)。调用之后,我就会被消耗掉,不复存在。 - 对闭包的限制:对闭包自身的限制最小。它可以随心所欲,甚至可以拿走环境变量的所有权。
- 调用者的自由:调用者只拥有一次调用的机会,自由度最低。
- 承诺内容:你只能调用我一次 (
将之前的逻辑反过来思考
用"承诺"的视角:
- 错误想法:
FnOnce有所有权,最厉害,所以它包含了FnMut和Fn。 - 正确的逻辑:一个闭包如果能做出
Fn的承诺(最强承诺),那么它自然也能满足FnMut(中等承诺)和FnOnce(最弱承诺)的要求。
这就像一个人的信用评级:
- 一个能被评为 AAA 级信用 (
Fn) 的人,向他借钱(调用他)风险极低,可以随时借。他自然也满足 AA 级 (FnMut) 和 A 级 (FnOnce) 的标准。 - 一个被评为 AA 级信用 (
FnMut) 的人,满足不了 AAA 级的苛刻标准,但他肯定满足 A 级的基本标准。 - 一个只有 A 级信用 (
FnOnce) 的人,意味着和他交易有风险,只能“一次性买卖”,他肯定满足不了 AA 级和 AAA 级的要求。
所以,这个关系是:
- 凡是
Fn,必然是FnMut和FnOnce。 - 凡是
FnMut,必然是FnOnce,但不一定是Fn。 FnOnce最为宽泛,它不承诺自己是FnMut或Fn。
代码验证
我们用一个具体的例子来印证这个理论。
// 一个函数,它要求一个“AAA信用”的闭包
fn call_repeatedly<F: Fn()>(closure: F) {
println!("--- Calling an Fn closure ---");
closure();
closure();
}
// 一个函数,它要求一个“AA信用”的闭包
fn call_mutably<F: FnMut()>(mut closure: F) {
println!("--- Calling an FnMut closure ---");
closure();
closure();
}
// 一个函数,它只要求“A信用”的闭包
fn call_once<F: FnOnce()>(closure: F) {
println!("--- Calling an FnOnce closure ---");
closure();
}
fn main() {
let mut my_string = String::from("Hello");
let owned_string = String::from("World");
// 1. 这是一个 Fn 闭包,因为它只对 my_string 进行了不可变借用。
// 它做出了最强的承诺。
let closure_fn = || {
println!("Fn says: {}", my_string);
};
// 2. 这是一个 FnMut 闭包,因为它对 my_string 进行了可变借用。
// 它只能做出中等承诺。
let mut closure_fn_mut = || {
my_string.push_str("!");
println!("FnMut says: {}", my_string);
};
// 3. 这是一个 FnOnce 闭包,因为它夺走了 owned_string 的所有权。
// 它只能做出最弱的承诺。
let closure_fn_once = || {
let consumed = owned_string;
println!("FnOnce says: {}", consumed);
};
// --- 开始验证 ---
// `closure_fn` (AAA级) 可以满足所有要求
call_repeatedly(closure_fn);
call_mutably(closure_fn);
call_once(closure_fn);
println!("\n");
// `closure_fn_mut` (AA级) 满足不了 AAA 级的要求
// call_repeatedly(closure_fn_mut); // 编译错误!因为它改变了环境,不满足 Fn 的要求
call_mutably(&mut closure_fn_mut);
call_once(&mut closure_fn_mut);
println!("\n");
// `closure_fn_once` (A级) 只能满足最基本的要求
// call_repeatedly(closure_fn_once); // 编译错误!
// call_mutably(closure_fn_once); // 编译错误!因为它被调用后就没了,不能调用第二次
call_once(closure_fn_once);
// call_once(closure_fn_once); // 再次调用也会编译错误,因为它已经被消耗了
}
总结
在 Rust 的 Trait 系统中,一个类型如果满足更强的承诺(Fn),它就能被用在要求较弱承诺(FnMut, FnOnce)的任何地方。这就是为什么 Fn 是最小、最核心的那个集合。
来自开放网络的回应
引用、喜欢、转发和站外回复会通过 Webmention 回到这里。
接收与展示代码已经就绪,注册正式域名后即可开始收集回应。
评论
评论由 GitHub Discussions 保存和管理。