Rust async块消费变量时为何无需显式move即可通过编译
Rust async块生命周期编译差异问题
问题场景
- 以下基于Tokio runtime编写的Rust程序可以正常编译通过:
#[tokio::main] async fn main() { async fn g(x: String) {} let f = || { let y: String = String::from("a").clone(); return async { println!("{}", &y); return g(y).await; }}; }
- 如果删除上述代码中的
return g(y).await;行,编译会直接失败,抛出E0373错误:
error[E0373]: async block may outlive the current function, but it borrows `y`, which is owned by the current function --> src/main.rs:35:22 | 35 | return async { | ______________________^ 36 | | println!("{}", &y); | | - `y` is borrowed here 37 | | // return g(y).await; 38 | | }}; | |_____^ may outlive borrowed value `y` | note: async block is returned here --> src/main.rs:35:16 | 35 | return async { | ________________^ 36 | | println!("{}", &y); 37 | | // return g(y).await; 38 | | }}; | |_____^ help: to force the async block to take ownership of `y` (and any other referenced variables), use the `move` keyword | 35 | return async move { | ++++
- 核心疑问:为什么保留
return g(y).await;的原始版本代码,不会触发上述生命周期编译错误?
原因解释
这个现象是Rust编译器对async块变量捕获的自动推断规则导致的:编译器不会死板地默认所有async块都用借用捕获变量,而是会根据块内对变量的实际使用方式,自动选择借用捕获或者所有权转移捕获,不需要开发者手动加move关键字也能满足安全校验。
- 删除
g(y).await行的版本报错逻辑
这个修改后的版本里,async块对y的唯一使用场景是println!宏中的不可变借用,编译器默认会采用借用方式捕获y。但这个async块是闭包的返回值,会逃逸出闭包的作用域,生命周期比闭包本身更长;而y是闭包内部创建的局部变量,闭包执行结束后y就会被销毁,此时async块持有的y的借用就会变成悬垂引用,违反Rust的内存安全规则,因此编译器直接抛出错误。 - 原始版本正常编译的逻辑
原始版本的async块中存在g(y).await调用,而函数g的入参类型是String,调用时必须获取y的所有权。编译器静态分析async块的执行路径时,检测到块内存在消耗y所有权的操作,就会自动将y的所有权转移到async块的内部状态中,效果和手动写async move完全一致。
至于块内println!对y的借用,完全不存在安全问题:async块的执行是严格线性顺序的,先执行打印逻辑时,y的所有权归async块自身所有,临时借用完全合法;打印完成后再调用g转移y的所有权,之后块内再也不会访问y,整个执行路径没有任何生命周期冲突,编译器可以验证其安全性,自然不会报错。
内容的提问来源于stack exchange,提问作者Test
相关产品推荐
相关产品推荐

