1. Fn, FnMut, FnOnce
1.1. 讨论的范畴:闭包 or 通常函数
讨论这三个 trait 时,通常是关于闭包的。普通的 rust 函数被视为实现了以上所有三个trait。可以用下面这段代码证明:
fn apply_fn<T>(value: T, f: impl Fn(T) -> T) -> T {
f(value)
}
fn apply_fnmut<T>(value: T, mut f: impl FnMut(T) -> T) -> T {
f(value)
}
fn apply_fnonce<T>(value: T, f: impl FnOnce(T) -> T) -> T {
f(value)
}
fn square(num: i32) -> i32 {
num * num
}
fn main() {
let result = apply_fn(5, square);
let result = apply_fnmut(5, square);
let result = apply_fnonce(5, square);
println!("{}", result);
}
1.2. 三个 Fn
系列 trait 的区别
对于闭包来说,编译期会自动为他们实现合适的 Fn
系列 trait。分三种:
FnOnce
约束最强,表示闭包只能被执行一次,通常是由于取得了捕获值的所有权,或者闭包本身会被消耗FnMut
约束次强,表示闭包可以被多次调用,且持有捕获变量的可变引用。Fn
约束最弱,表示闭包可以被重复调用,且持有捕获变量的不可变引用。
实际上,FnOnce
是 FnMut
的超集(supertrait),而 FnMut
又是 Fn
的超集。即可以用伪码理解:
trait FnOnce {}
trait FnMut: FnOnce {}
trait Fn: FnMut {}
因此,一个较弱的闭包可以被传入更强的约束中,例如一个只实现了 Fn
的闭包可以被作为参数传入要求是 FnOnce
的场合。从逻辑来说,这样也是完全成立的。
1.3. 关于 FnOnce
的补充:调用后被 move,以及闭包实现 Copy
Rust 编译器是怎么限制 FnOnce
只能调用一次的呢?在 rustbook 中译版对闭包的介绍章节中,有一个尝试重复调用的例子:
fn fn_once<F>(func: F)
where
F: FnOnce(usize) -> bool,
{
println!("{}", func(3));
println!("{}", func(4));
}
这段代码会报错:
error[E0382]: use of moved value: `func`
--> src/main.rs:118:20
|
113 | fn fn_once<F>(func: F)
| ---- move occurs because `func` has type `F`, which does not implement the `Copy` trait
...
117 | println!("{}", func(3));
| ------- `func` moved due to this call
118 | println!("{}", func(4));
| ^^^^ value used here after move
|
note: this value implements `FnOnce`, which causes it to be moved when called
--> src/main.rs:117:20
|
117 | println!("{}", func(3));
| ^^^^
help: consider further restricting this bound
|
115 | F: FnOnce(usize) -> bool + Copy,
| ++++++
根据报错信息可知,FnOnce
的闭包在调用后会被 move 掉,因此我们不再能调用它了。但是如果加上 Copy
的 trait bound,就能可以多次调用这个 func
。查询 GPT,得知如果一个闭包捕获的变量都是 Copy
的,那么它也是 Copy
。这两个性质都能体现出闭包本身就是一种 rust 类型,一个类型的变量被 move 掉以后自然不能再访问,而如果一个类型里拥有的引用都是 Copy
的,那么它自然也是 Copy
的。
1.4. 补充之二:闭包的底层原理
这部分资料来源于 Efficient Rust p14。闭包的实现原理是,编译期创建一个隐藏类,存放所有需要被闭包捕获的引用,然后将闭包的匿名函数体实现为此类的方法。