Rust Fn、FnMut 与 FnOnce
在闭包那一页,你学会了怎样写闭包、闭包怎样捕获变量,以及怎样修改它们。这一页回答接下来的问题:怎样写一个你自己的、能接收闭包的函数?又怎样从函数里把闭包交还出去?
答案是三个小小的 trait:Fn、FnMut 和 FnOnce。可以把它们想象成图书馆的规矩:有的读者只看书(Fn);有的会在页边写笔记(FnMut);还有的把书借回家,再也不还(FnOnce)。Rust 会看你的闭包做了什么,然后给它贴上对应的标签。刚开始觉得这几个 trait 有点吓人很正常,别担心:总共只有三个,而且编译器会替你选好。
为什么闭包需要 trait
Section titled “为什么闭包需要 trait”每个闭包都有一个独一无二的类型,由编译器自动生成。你没法写出这个类型的名字。即使两个闭包看起来一模一样,它们的类型也不同:
fn main() { let mut rules = vec![|price: u32| price * 2]; rules.push(|price: u32| price * 2);}error[E0308]: mismatched types --> src/main.rs:3:16 |3 | rules.push(|price: u32| price * 2); | ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure | = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait object编译器的意思是:没有两个闭包的类型是相同的,哪怕它们完全一样。所以函数没法说“给我一个类型为 X 的闭包”,而是说“给我任何能这样调用的类型”。这就是 trait 约束,和泛型、trait 那两页讲的一样:
fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 { rule(price)}
fn main() { let coupon = 15; let half_price = |price: u32| price / 2; let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80)); println!("{}", price_after(use_coupon, 80));}输出:
4065F: Fn(u32) -> u32 可以读作:“F 是某种类型,你可以传一个 u32 去调用它,它会还给你一个 u32。”这两个闭包的类型虽然不同,但都符合这个条件。
三个 trait:Fn、FnMut 和 FnOnce
Section titled “三个 trait:Fn、FnMut 和 FnOnce”编译器会看闭包的函数体对它捕获的值做了什么:
Fn:只读取它们。想调用多少次都行。FnMut:会修改它们。可以调用很多次,但需要mut。FnOnce:会把它们交出去(移出闭包)。只能调用一次,因为调用之后那个值就没了。
fn main() { let shop = String::from("街角面包店");
// Fn:只读取 `shop` let greet = || println!("欢迎光临{shop}!"); greet(); greet();
// FnMut:修改 `orders` let mut orders = 0; let mut take_order = || { orders += 1; println!("已接第 {orders} 单"); }; take_order(); take_order();
// FnOnce:把 `receipt` 交还给调用者 let receipt = String::from("2 个可颂,1 杯咖啡"); let hand_over = || receipt; let customer_copy = hand_over(); println!("顾客拿到:{customer_copy}");}输出:
欢迎光临街角面包店!欢迎光临街角面包店!已接第 1 单已接第 2 单顾客拿到:2 个可颂,1 杯咖啡hand_over 返回的是 receipt 本身,而不是一份副本。小票一旦交出去,闭包就没有东西可给了。所以它是 FnOnce。
下面这幅图值得记在脑子里。想象三个圆,一个套着一个:
- 最外面的大圆是
FnOnce。每个闭包都至少能被调用一次,所以所有闭包都在这个圆里。 - 里面是一个小一点的圆
FnMut:可以被反复调用的闭包。 - 最中间是
Fn:可以被反复调用,而且不改变任何东西的闭包。
所以每个 Fn 闭包同时也是 FnMut 和 FnOnce。这一点在你写函数时很重要:要求 FnOnce 的参数能接受任何闭包;要求 Fn 的参数是最挑剔的。
有一点常常让人意外:move 关键字并不决定闭包实现哪个 trait。move || println!("{shop}") 拿走了 shop 的所有权,但它只是读取,所以仍然是 Fn。决定这一点的只有闭包体。
编写接收闭包的函数
Section titled “编写接收闭包的函数”同一个约束有三种写法。它们的意思完全一样,挑你觉得最好读的就行:
// 1. 带约束的泛型参数fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x)}
// 2. 参数位置的 `impl Fn`:更短,不需要起名字fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(f(x))}
// 3. `where` 子句:约束变长时更整洁fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>where F: Fn(i32) -> i32,{ scores.iter().map(|&s| f(s)).collect()}
fn main() { let bonus = 10; let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50)); println!("{}", apply_twice(add_bonus, 50)); println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));}输出:
6070[50, 75, 100]你可能会好奇,为什么 add_bonus 能传三次?一个只读取捕获值的闭包复制起来很便宜,所以 Rust 每次都会复制它。
接下来看 FnMut 和 FnOnce。用 FnMut 时,参数需要 mut,因为调用闭包会改变它内部的东西。用 FnOnce 时,只能调用一次,仅此而已:
fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { for task in tasks { on_done(task); }}
fn on_shutdown<F: FnOnce()>(cleanup: F) { println!("正在关闭……"); cleanup();}
fn main() { let mut finished = Vec::new(); process_tasks(&["发送邮件", "备份数据库"], |task| { finished.push(task.to_string()); }); println!("已完成:{:?}", finished);
on_shutdown(|| { // 这个循环拿走了 `finished` 的所有权,所以这个闭包是 FnOnce for task in finished { println!("已归档:{task}"); } });}输出:
已完成:["发送邮件", "备份数据库"]正在关闭……已归档:发送邮件已归档:备份数据库一个好用的经验法则:只要求你的函数能接受的最宽松的 trait。如果你只调用闭包一次,就要求 FnOnce,这样调用者传什么闭包都行。标准库也是这么做的:Option::map 接收 FnOnce,而 Iterator::map 接收 FnMut。
约束选错了会怎样
Section titled “约束选错了会怎样”编译器会抓住每一处不匹配,而且这里的报错信息非常友好。假设一个函数要求 Fn,但你的闭包修改了一个计数器:
fn run_report<F: Fn()>(f: F) { f(); f();}
fn main() { let mut runs = 0; run_report(|| { runs += 1; println!("第 {runs} 次运行报告"); });}error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure --> src/main.rs:9:9 |1 | fn run_report<F: Fn()>(f: F) { | - change this to accept `FnMut` instead of `Fn`...8 | run_report(|| { | ---------- -- in this closure | | | expects `Fn` instead of `FnMut`9 | runs += 1; | ^^^^^^^^^ cannot assign它甚至直接告诉了你修复方法:改成接收 FnMut(change this to accept FnMut instead of Fn),并给参数加上 mut。改完之后,程序会打印 第 1 次运行报告 和 第 2 次运行报告:
fn run_report<F: FnMut()>(mut f: F) { f(); f();}另一个经典错误是把 FnOnce 闭包调用两次:
fn main() { let report = String::from("第三季度销售额"); let send = || { let sent = report; println!("正在发送{sent}"); }; send(); send();}error[E0382]: use of moved value: `send` --> src/main.rs:8:5 |7 | send(); | ------ `send` moved due to this call8 | send(); | ^^^^ value used here after move |note: closure cannot be invoked more than once because it moves the variable `report` out of its environment --> src/main.rs:4:20 |4 | let sent = report; | ^^^^^^note 说得很直白:闭包把变量 report 移出了它的环境,所以不能被调用多次。第一次调用就把 report 交出去了,第二次调用时已经什么都不剩。如果你真的需要调用两次,就改成借用(let sent = &report;),这样闭包就变成了 Fn。如果你把这样的闭包传给一个要求 Fn 的函数,会得到类似的报错:error[E0507]: cannot move out of report, a captured variable in an Fn closure。
用 impl Fn 返回闭包
Section titled “用 impl Fn 返回闭包”函数也可以构造一个闭包,然后把它交还出去。返回类型写成 impl Fn(i32) -> i32,意思是“某种闭包类型,你不需要知道它叫什么”:
fn make_adder(amount: i32) -> impl Fn(i32) -> i32 { move |x| x + amount}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 { move |x| x * factor}
fn main() { let add_tip = make_adder(5); let triple = make_multiplier(3);
println!("{}", add_tip(20)); println!("{}", triple(7)); println!("{}", add_tip(triple(10)));}输出:
252135为什么要 move?没有它,闭包只会借用 amount。但 amount 属于 make_adder,make_adder 一返回它就消失了,闭包手里拿着的就是一个指向空无的引用。Rust 不允许这样:
error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function --> src/main.rs:2:5 |2 | |x| x + amount | ^^^ ------ `amount` is borrowed here | | | may outlive borrowed value `amount` |help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword |2 | move |x| x + amount | ++++报错的意思是:闭包可能比当前函数活得更久,但它借用了当前函数拥有的 amount;help 建议用 move 关键字强制闭包取得所有权。加上 move 之后,闭包会把自己的那份 amount 打包带走。就像把午饭随身带着,而不是放在别人家的冰箱里。
装箱的闭包:多个闭包,同一种类型
Section titled “装箱的闭包:多个闭包,同一种类型”impl Fn 代表的是一个隐藏的类型。所以你不能在 if 分支返回一个闭包,在 else 分支返回另一个闭包;也不能把不同的闭包放进同一个 Vec。这两件事都要靠把每个闭包装进盒子:Box<dyn Fn(f64) -> f64> 的意思是“任何具有这种形状、存放在堆上的闭包”。这和 trait 对象那一页讲的是同一个思路。
下面是一个结账程序,按顺序应用一串带名字的价格规则:
fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { if express { Box::new(move |total| total + fee * 2.0) } else { Box::new(move |total| total + fee) }}
fn main() { let member_percent = 10.0; let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![ ("会员折扣", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))), ("优惠券", Box::new(move |total: f64| total - coupon)), ("标准运费", shipping_rule(false, 3.5)), ];
let mut total = 80.0; println!("起始:{total:.2}"); for (name, rule) in &rules { total = rule(total); println!("{name}之后:{total:.2}"); }}输出:
起始:80.00会员折扣之后:72.00优惠券之后:67.00标准运费之后:70.50装箱需要一次很小的堆分配,而且每次调用都要经过一个指针。这几乎从来不会成为问题。只有一种闭包类型时用 impl Fn,有多种时用 Box<dyn Fn>。
在结构体里存放闭包
Section titled “在结构体里存放闭包”结构体也可以存放闭包。因为你写不出闭包类型的名字,所以要让结构体对它泛型化。下面是一个小按钮,被点击时会运行一段代码:
struct Button<F: Fn()> { label: String, on_click: F,}
impl<F: Fn()> Button<F> { fn click(&self) { println!("[{}] 被点击", self.label); (self.on_click)(); }}
fn main() { let user = String::from("Ada"); let save = Button { label: String::from("保存"), on_click: move || println!("正在保存 {user} 的草稿"), };
save.click(); save.click();}输出:
[保存] 被点击正在保存 Ada 的草稿[保存] 被点击正在保存 Ada 的草稿注意 (self.on_click)() 里的括号。它告诉 Rust:“先取出这个字段,再调用它”。如果按钮每次被点击都需要修改它捕获的值,就用 F: FnMut(),并让方法接收 &mut self。
函数指针:fn(i32) -> i32
Section titled “函数指针:fn(i32) -> i32”普通函数也可以传来传去。它们的类型用小写的 fn 来写,比如 fn(i32) -> i32。这叫作函数指针(function pointer),就是一段代码的普通地址。什么都不捕获的闭包也能变成函数指针:
fn double(x: i32) -> i32 { x * 2}
fn square(x: i32) -> i32 { x * x}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 { op(x)}
fn main() { println!("{}", apply_op(double, 4)); println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1]; let mut value = 3; for step in steps { value = step(value); } println!("{value}");}输出:
8337函数指针都是同一种类型,所以不用装箱就能放进数组。问题在于,捕获了东西的闭包不能变成函数指针。编译器会说:closures can only be coerced to fn types if they do not capture any variables(只有不捕获任何变量的闭包才能转换成 fn 类型)。好消息是反过来没问题:每个普通函数也都实现了 Fn、FnMut 和 FnOnce,所以你可以把 double 传给前面的 apply。这就是为什么大多数代码使用闭包 trait:两种都能接收。
忘了给 FnMut 参数加 mut。 调用 FnMut 会改变它,所以它的绑定必须是 mut:
error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable --> src/main.rs:3:9 |3 | on_done(task); | ^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) { | +++从 -> impl Fn 返回不同的闭包。 两个闭包就是两种类型,哪怕它们看起来很像:
fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 { if express { move |total| total + fee * 2.0 } else { move |total| total + fee }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if express {3 | | move |total| total + fee * 2.0 | | ------------------------------ | | | | | the expected closure | | expected because of this4 | | } else {5 | | move |total| total + fee | | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure6 | | } | |_____- `if` and `else` have incompatible types | = note: expected closure `{closure@src/main.rs:3:9: 3:21}` found closure `{closure@src/main.rs:5:9: 5:21}` = note: no two closures, even if identical, have the same type = help: consider boxing your closure and/or using it as a trait objecthelp: you could change the return type to be a boxed trait object |1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new(move |total| total + fee * 2.0)4 | } else {5 ~ Box::new(move |total| total + fee) |照编译器的建议做:返回 Box<dyn Fn(f64) -> f64>,并把每个闭包包进 Box::new(...)。
像调用方法一样调用闭包字段。 写成 self.on_click(),Rust 会去找一个名叫 on_click 的方法:
error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope --> src/main.rs:9:14 |9 | self.on_click(); | ^^^^^^^^ field, not a method |help: to call the type parameter stored in `on_click`, surround the field access with parentheses |9 | (self.on_click)(); | + +报错指出 on_click 是字段而不是方法(field, not a method),并建议用括号把字段访问包起来。
1. 取号机
Section titled “1. 取号机”写一个函数 ticket_dispenser(start: u32),它返回一个闭包。每次调用这个闭包,都会给出下一个号码:先是 start,然后是 start + 1,依此类推。做两台取号机(面包店从 100 开始,药店从 1 开始),证明它们各自独立计数。提示:闭包会修改它自己的计数器,所以返回类型是 impl FnMut() -> u32,而且你需要 move。
查看答案
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 { let mut next = start; move || { let ticket = next; next += 1; ticket }}
fn main() { let mut bakery = ticket_dispenser(100); let mut pharmacy = ticket_dispenser(1);
println!("面包店:{}", bakery()); println!("面包店:{}", bakery()); println!("药店:{}", pharmacy()); println!("面包店:{}", bakery());}输出:
面包店:100面包店:101药店:1面包店:102每台取号机都把自己的 next 移进了闭包,所以它们从不共用计数器。变量 bakery 和 pharmacy 需要 mut,因为它们是 FnMut。
2. 重试助手
Section titled “2. 重试助手”写一个 retry(max_attempts, task)。它会反复调用 task,直到返回 Ok,或者已经尝试了 max_attempts 次。每次尝试失败都打印一行。用一个先以 "服务器繁忙" 失败两次、然后成功的闭包来测试它。提示:测试用的闭包会统计自己被调用的次数,所以约束是 FnMut() -> Result<String, String>,参数需要 mut。
查看答案
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>where F: FnMut() -> Result<String, String>,{ for attempt in 1..=max_attempts { match task() { Ok(value) => return Ok(value), Err(e) => println!("第 {attempt} 次尝试失败:{e}"), } } Err(String::from("放弃了"))}
fn main() { let mut calls = 0; let result = retry(5, || { calls += 1; if calls < 3 { Err(String::from("服务器繁忙")) } else { Ok(String::from("数据已下载")) } }); println!("调用 {calls} 次后得到 {:?}", result);}输出:
第 1 次尝试失败:服务器繁忙第 2 次尝试失败:服务器繁忙调用 3 次后得到 Ok("数据已下载")retry 会多次调用任务,所以 FnOnce 不行;任务会修改 calls,所以 Fn 也不行。FnMut 刚刚好。retry 返回后,闭包的任务就结束了,所以 main 又可以读取 calls 了。
- 每个闭包都有自己独一无二的类型,所以函数通过
F: Fn(i32) -> i32这样的 trait 约束来接收闭包。 Fn只读取捕获值,FnMut修改它们,FnOnce把它们移出去,只能调用一次。决定这一点的是闭包体,而不是move。- 每个
Fn同时也是FnMut和FnOnce。只要求你的函数需要的最宽松的 trait。 - 约束可以写成
<F: Fn(..)>、impl Fn(..),或者放在where子句里。FnMut参数需要mut。 - 返回一个闭包用
-> impl Fn(..)(通常要配合move)。要从不同分支返回不同的闭包,或者把它们放进Vec,就用Box<dyn Fn(..)>。 fn(i32) -> i32是函数指针。普通函数和不捕获变量的闭包都符合它。
小测验
1. 一个闭包对捕获的变量执行 total += price。它实现的最严格的 trait 是哪个?
2. 你的函数只调用它的闭包参数一次。哪个约束能接受最多的闭包?
3. 为什么 make_adder(amount: i32) -> impl Fn(i32) -> i32 需要写成 move |x| x + amount?
4. 你想要一个 Vec,里面放好几个不同的折扣闭包。元素需要什么类型?
得分: 0 / 4
- 将捕获的值移出闭包 — Rust 程序设计语言:官方教程讲解编译器怎样选择
Fn、FnMut或FnOnce。 Fn、FnMut和FnOnce— 标准库文档(英文):每个 trait 的定义,附简短示例,说明什么时候该用它。- 输入参数 — 通过例子学 Rust:分别使用三种约束接收闭包的可运行示例。
- 输出参数 — 通过例子学 Rust:用
impl Fn返回闭包,以及为什么需要move。 - Closure types — The Rust Reference(英文):闭包怎样捕获值、实现哪些 trait 的精确规则。