Intro
写这篇post的原因是, 在给机器人写 warp server 的代码时, 遇到了下面这个编译错误:
warp::path!("select" / "coordinate")
.and(warp::post())
.and(warp::body::content_length_limit(1024))
.and(warp::body::json())
.then(|SelectCoordinate{x, y}: SelectCoordinate| async move {
if let Err(_) = update_sender.send(UpdateMessage::Update(StateUpdate::AddNewRouteByCoordinate(crate::robot::Coordinate(x, y)))).await {
return_code(1)
} else {
return_code(0)
}
})
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
--> robot_admin/src/route.rs:92:15
|
92 | .then(|SelectCoordinate{x, y}: SelectCoordinate| async move {
| ---- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this closure implements `FnOnce`, not `Fn`
| |
| the requirement to implement `Fn` derives from here
93 | if let Err(_) = update_sender.send(UpdateMessage::Update(StateUpdate::AddNewRouteByCoordinate(crate:...
| ------------- closure is `FnOnce` because it moves the variable `update_sender` out of
its environment
|
= note: required for `[closure@robot_admin/src/route.rs:92:15: 92:57]` to implement `warp::generic::Func<(SelectCoordinat
e,)>`
For more information about this error, try `rustc --explain E0525`.
这段代码生成一个 POST /select/coordinate
的路由, 它接收一个json
form并更新机器人的路径信息. 这个更新的工作交由一个 "异步闭包" 来完成.
我并 " 没有将 update_sender
消耗或者移动所有权 ", 但是这个闭包却是
FnOnce
而不是 Fn
, 不符合要求, 编译失败.
这种情况可以简化成下面的代码:
use std::future::Future;
#[derive(Debug, Clone)]
struct S { pub a: i32 }
fn take_fn<T: Future<Output = ()>>(_f: impl Fn() -> T) -> () {}
fn take_fnonce<T: Future<Output = ()>>(_f: impl FnOnce() -> T) -> () {}
fn main() {
// TODO Why is `f` a `FnOnce`, which means that `f` takes the ownership of s and consumes it.
let s = S { a: 1 };
let f = || async move {
println!("a is {:?}", s);
};
take_fn(f); // ERROR!
take_fnonce(f); // OK!
println!("Hello world!");
}
f
是一个异步闭包吗?
rust
的异步闭包的语法是 async [move] |_| { /* block */ }
.
但是注意看 f
的声明, async move
写在了参数的后面,
说明这不是一个异步闭包, 而只是一个普通的闭包, 它返回一个 impl Future
.
take_fn*
的签名也证明了这一点.
异步闭包现在是一个不稳定的特性,
一般都通过一个返回 Future
/ 异步 block 的普通闭包实现. FnOnce
说明了我们写的这个 闭包确实消耗了所有权, 只能执行一次 (对比 Fn
,
它并不消耗所有权, 所以可以执行任意多次).
为什么是 FnOnce
?
f
返回的异步 block 是 move 块, 说明这个块会获得捕获变量的所有权.
包裹这个异步块的闭包捕获了 s
, 随后又被异步块获取了 所有权.
异步块作为返回值, 将 s
的所有权转移到了外部, 这个时候闭包已经不拥有
s
的所有权了, 所以为 FnOnce
.
所有权的转移过程也可以像下面这样展示:
let f = || { // Closure 从环境中捕获了 s
async move { // Async Block 使用 move 捕获, 直接获取所有权而不是借用
println!("a is {:?}", s); // Async Block 从 Closure 中捕获了 s
} // Async Block 结束
}; // s 的所有权和 Async Block 作为返回值一同转移到了外部
解决的办法就是在闭包中保留所有权, 比如说直接 Clone(&self)
或者使用
Arc
来进行管理.
let s = S { a: 1 };
let f = move || {
let s = s.clone();
async move {
println!("a is {:?}", s);
}
};
take_fn(f); // TADAA!