Rust 错误 E0373:closure may outlive the current function(闭包可能比函数存活更久)
E0373 的意思是:一个闭包借用了某个局部变量,但拥有这个变量的函数结束之后,这个闭包可能还在运行(或者还存在)。你通常会在第一次调用 thread::spawn,或者第一次从函数里返回闭包时遇到它。好消息是:编译器已经告诉你怎么修了,而且几乎总是一个词:move。本页先给出修法,再解释原因。这个系列的其他错误,请看 Rust 编译错误详解。
error[E0373]: closure may outlive the current function, but it borrows `scores`, which is owned by the current function --> src/main.rs:5:32 |5 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `scores`6 | println!("Scores: {scores:?}"); // ❌ 只借用了 scores | ------ `scores` is borrowed here |note: function requires argument type to outlive `'static` --> src/main.rs:5:18 |5 | let handle = thread::spawn(|| { | __________________^6 | | println!("Scores: {scores:?}"); // ❌ 只借用了 scores7 | | }); | |______^help: to force the closure to take ownership of `scores` (and any other referenced variables), use the `move` keyword |5 | let handle = thread::spawn(move || { | ++++第一行的意思是“闭包可能比当前函数活得更久,但它借用了归当前函数所有的 scores”。help 建议:要强制闭包获得 scores(以及其他被引用的变量)的所有权,就使用 move 关键字。
产生这个错误的程序是这样的:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(|| { println!("Scores: {scores:?}"); // ❌ 只借用了 scores }); handle.join().unwrap();}在 async 代码里,你会看到同一个错误代码,措辞稍有不同:“async block may outlive the current function, but it borrows user_id”(async 代码块可能比当前函数活得更久,但它借用了 user_id)。这是同一个错误,修法也一样(async move)。
这个错误是什么意思
Section titled “这个错误是什么意思”从上往下读这些标签:
^^ may outlive borrowed value(可能比被借用的值活得更久)指向闭包||。Rust 无法保证在main继续往下执行之前,这个闭包已经结束。`scores` is borrowed here(scores在这里被借用)指向闭包使用scores的地方。因为println!只需要读取它,闭包捕获的是scores的引用,而不是这个 vector 本身。function requires argument type to outlive 'static(函数要求参数类型比'static活得更久)解释了为什么这是个问题:thread::spawn只接受能在程序需要时一直活着的闭包。'static的意思是“没有借用任何可能消失的东西”。
想象你把笔记本借给朋友,说“用完还给我”,可你马上就要搬去另一个城市。如果朋友做得慢,笔记本就无处可还了。Rust 拒绝这种约定,要求你干脆把笔记本送出去。
Rust 为什么要这样做
Section titled “Rust 为什么要这样做”闭包会用开销最小的可行方式捕获变量。如果它只是读取一个变量,就用 & 借用它。对于马上就运行的闭包,比如传给 .map() 或 .filter() 的那些,这样做非常好。更多内容见闭包这一页。
新线程就不一样了。它可以运行任意长的时间,而且除非你调用 join,否则 main 不会等它。如果线程持有 scores 的引用,而 main 先结束了,scores 就会被丢弃,线程读到的就是已释放的内存。这就是“悬垂引用”,C 和 C++ 多线程代码里最常见的 bug 之一。Rust 在编译时就阻止了它。
返回闭包时也会发生同样的事。函数返回时,它的局部变量(以及参数)都会被丢弃。如果闭包还借用着它们,就会指向不存在的东西。
修复 1:加上 move(通常的答案)
Section titled “修复 1:加上 move(通常的答案)”几乎每次都可以用这个方法,这也正是 rustc 的建议。move 让闭包获得它所用变量的所有权,这些变量会跟着闭包一起走:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(move || { let total: i32 = scores.iter().sum(); println!("分数:{scores:?},总分 {total}"); }); handle.join().unwrap();}输出:
分数:[90, 85, 77],总分 252对于返回闭包的函数,修法也一样。没有 move 时,|x| x + n 借用的是参数 n,而 make_adder 一返回,n 就没了。加上 move 后,每个闭包都保留一份自己的 n:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n}
fn main() { let add_five = make_adder(5); let add_ten = make_adder(10); println!("{}", add_five(1)); println!("{}", add_ten(1));}输出:
611move 不会改变你得到的是哪一种闭包 trait。一个只读取数据的 move 闭包仍然是 Fn,可以调用很多次。
修复 2:还要用这个值时,先克隆一份
Section titled “修复 2:还要用这个值时,先克隆一份”加上 move 之后,变量就归闭包所有了。如果 main 之后还要用它,你会得到 E0382(“borrow of moved value”,借用已移动的值),rustc 会建议你在移动之前先克隆。给闭包做一份副本,原来的留着:
use std::thread;
fn main() { let guests = vec![String::from("Ana"), String::from("Ben")]; let guests_for_thread = guests.clone();
let handle = thread::spawn(move || { println!("线程看到了 {} 位客人", guests_for_thread.len()); }); handle.join().unwrap();
println!("main 里还有:{guests:?}");}输出:
线程看到了 2 位客人main 里还有:["Ana", "Ben"]克隆一个小列表开销很小,完全没问题。如果数据很大,或者有很多线程都需要它,就用 Arc(修复 3),让它们共享同一份数据。
修复 3:用 Arc 在多个线程之间共享
Section titled “修复 3:用 Arc 在多个线程之间共享”当多个线程需要读取同一份数据时,就用这个方法。Arc(“atomically reference counted”,原子引用计数)让多个所有者共享同一个值。Arc::clone 只复制一个指针并把计数加一,数据本身不会被复制:
use std::sync::Arc;use std::thread;
fn main() { let prices = Arc::new(vec![4.5, 2.0, 3.5]); let mut handles = Vec::new();
for discount in [0.0, 0.1, 0.5] { let prices = Arc::clone(&prices); // 每个线程一个句柄 handles.push(thread::spawn(move || { let total: f64 = prices.iter().sum(); total * (1.0 - discount) })); }
for handle in handles { println!("总价:{:.2}", handle.join().unwrap()); } println!("价格:{prices:?}");}输出:
总价:10.00总价:9.00总价:5.00价格:[4.5, 2.0, 3.5]单独的 Arc 只提供只读共享。如果线程还需要修改数据,就把它包成 Arc<Mutex<T>>。参见并发和共享状态。
修复 4:用 std::thread::scope 安全地借用
Section titled “修复 4:用 std::thread::scope 安全地借用”当线程只是在一小段工作里用到数据,而你宁愿借用也不想移动或克隆时,就用这个方法。在 thread::scope 里启动的线程,保证会在 scope 返回之前结束,所以借用局部变量是安全的,也不需要 move:
use std::thread;
fn main() { let scores = vec![90, 85, 77, 64]; let (first, second) = scores.split_at(2);
let (a, b) = thread::scope(|s| { let h1 = s.spawn(|| first.iter().sum::<i32>()); let h2 = s.spawn(|| second.iter().sum::<i32>()); (h1.join().unwrap(), h2.join().unwrap()) }); // 到这里两个线程都已经结束
println!("{a} + {b} = {}", a + b); println!("仍然归我们所有:{scores:?}");}输出:
175 + 141 = 316仍然归我们所有:[90, 85, 77, 64]对于“把工作拆开,再使用结果”这类需求,作用域线程是最干净的答案。共享状态这一课就是从它讲起的。
在循环里创建线程。 即使是一个普通的数字,只要闭包只读取它,也会被借用:
for worker in 1..=3 { handles.push(thread::spawn(|| { println!("{worker} 号工人开工了"); // ❌ 借用了 `worker` }));}修复:thread::spawn(move || ...)。worker 是 i32,所以 move 只是把它复制进每个线程。
返回装箱的闭包。 在返回 Box<dyn Fn() -> String> 的函数里写 Box::new(|| greeting.clone()),会借用局部变量 greeting,rustc 还会加上 note: closure is returned here(闭包在这里被返回)。修复:Box::new(move || greeting.clone())。更多内容见 Fn、FnMut 与 FnOnce。
异步任务(Tokio 等)。 tokio::spawn 和 thread::spawn 一样:任务可能在当前函数返回很久之后还在运行,所以它需要 'static 数据。写 tokio::spawn(async { ... }) 并在里面使用局部变量,会得到 “async block may outlive the current function”。修复:tokio::spawn(async move { ... }),并把任务外面还要用的东西克隆一份或者 Arc::clone。参见 Async/await。
- E0382:borrow of moved value:在你加上
move、又在闭包外面再次使用变量之后出现。先克隆(修复 2),或者用Arc(修复 3)。 - E0597:borrowed value does not live long enough:近亲。值已经被丢弃,指向它的引用却还在使用。
- E0515:cannot return reference to local variable:同样是“函数一返回就没了”的问题,只不过是普通的引用,而不是闭包。
- E0106:缺少生命周期:当你想返回一个借用了某些东西的引用(或闭包类型)时会出现。
- 所有编译错误:错误页面的完整列表。
小测验
1. 一个线程闭包没用 move 就打印了 name。为什么会得到 E0373?
2. 你加上了 move,但 main 在创建线程之后还需要打印这个 vector。最简单的修法是什么?
3. 哪个工具能让线程不用 move 就借用局部变量?
得分: 0 / 3
- E0373 — Rust error codes index(英文):这个错误的官方说明,包括
move的修法。 - 将 move 闭包与线程一同使用 — Rust 程序设计语言:一步步讲解的正是这个错误。
- 闭包:可以捕获其环境的匿名函数 — Rust 程序设计语言:闭包怎样通过引用、可变引用或值来捕获变量。
movekeyword — standard library docs(英文):move对闭包的捕获方式有什么影响,附有简短示例。std::thread::scope— standard library docs(英文):能够安全借用局部数据的作用域线程。