跳转到内容

Rust Fn、FnMut 与 FnOnce

在闭包那一页,你学会了怎样写闭包、闭包怎样捕获变量,以及怎样修改它们。这一页回答接下来的问题:怎样写一个你自己的、能接收闭包的函数?又怎样从函数里把闭包交还出去?

答案是三个小小的 trait:Fn、FnMut 和 FnOnce。可以把它们想象成图书馆的规矩:有的读者只看书(Fn);有的会在页边写笔记(FnMut);还有的把书借回家,再也不还(FnOnce)。Rust 会看你的闭包做了什么,然后给它贴上对应的标签。刚开始觉得这几个 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));
}

输出:

40
65
▶ 动手试一试

F: Fn(u32) -> u32 可以读作:“F 是某种类型,你可以传一个 u32 去调用它,它会还给你一个 u32。”这两个闭包的类型虽然不同,但都符合这个条件。

编译器会看闭包的函数体对它捕获的值做了什么:

  • 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。决定这一点的只有闭包体。

同一个约束有三种写法。它们的意思完全一样,挑你觉得最好读的就行:

// 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]));
}

输出:

60
70
[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。

编译器会抓住每一处不匹配,而且这里的报错信息非常友好。假设一个函数要求 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 call
8 | 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(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)));
}

输出:

25
21
35
▶ 动手试一试

为什么要 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>。

结构体也可以存放闭包。因为你写不出闭包类型的名字,所以要让结构体对它泛型化。下面是一个小按钮,被点击时会运行一段代码:

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 来写,比如 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}");
}

输出:

8
3
37
▶ 动手试一试

函数指针都是同一种类型,所以不用装箱就能放进数组。问题在于,捕获了东西的闭包不能变成函数指针。编译器会说: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 this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `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 object
help: 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),并建议用括号把字段访问包起来。

写一个函数 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。

写一个 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