I want to share an issue that took me a lot of time to trace the root course.
This is not a new issue of simular things, however, although I have been used to fix things like that without async/await
, async/await
obstacles the error message and makes the reason very hard to point out in the first place.
Here is a simplified version of the code (Playground):
#![feature(
await_macro,
async_await,
futures_api,
optin_builtin_traits
)]
use std::future::Future;
struct Foo;
impl Foo {
fn foo(&self) -> Option<()> { Some(()) }
}
impl !Sync for Foo{}
async fn bar() {
let f = Foo;
if let Some(v) = f.foo() {
await!(async{})
}
}
async fn buz(f: impl Future<Output=()> + Send) {
await!(f)
}
fn main(){
buz(bar());
}
error message:
error[E0277]: `Foo` cannot be shared between threads safely
--> src/main.rs:27:5
|
27 | buz(bar());
| ^^^ `Foo` cannot be shared between threads safely
|
Even when the code simplified like the above, I believe it is not clear to many people to guess the reason. After all, we do not attempt to send a &Foo
to anywhere, or do we?
Actually the if let Some(v) = f.foo()
line betrayed us. It looks so innocent that it just call foo
on f
, isn’t it? But it actually keeps the reference of f
though the whole block, so the reference will have to go through an await
boundry, and therefore the returning Future
of the outer async
block have to keep a reference of it. As we require the Future
to be Send
, Foo
thus have to be Sync
.
The fix, of cause is to use a temporary variable for the pattern match source.
let tmp = f.foo();
if let Some(v) = tmp {...
(There will be warnings but I am only demostrating and at least it will compile)
This kind of error is not new to me - I fixed countless such error before, mostly when I attempt to move things out of a struct
when pattern matching an expression using a reference of the struct
. However in such cases the error message clearly tell me what I attempted to do and therefore I can quickly spot the error and apply the solution above.
Now, as I know that the strategy to create a temporary variable would always work, and the cases that requires a reference to live though out the match block are
- relatively rare;
- even in such a case the temporary variable is still good enough to catch the lifetime of the borrow,
Why do we not make this the default behaviour? Am I right to assume that match exp {...}
should always behave the same as let tmp=exp; match tmp {...}
?