Ошибка 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` is borrowed here |note: function requires argument type to outlive `'static` --> src/main.rs:5:18 |5 | let handle = thread::spawn(|| { | __________________^6 | | println!("Оценки: {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, которым владеет эта функция. Подсказка в конце предлагает ключевое слово move, чтобы замыкание забрало scores (и все остальные используемые переменные) во владение.
Вот программа, которая выдаёт эту ошибку:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(|| { println!("Оценки: {scores:?}"); // ❌ только заимствует scores }); handle.join().unwrap();}В асинхронном коде ты увидишь тот же код ошибки с немного другими словами: «async block may outlive the current function, but it borrows user_id» («async-блок может пережить текущую функцию, но заимствует user_id»). Это та же ошибка, и исправление у неё то же (async move).
Что это значит
Заголовок раздела «Что это значит»Прочитай пометки сверху вниз:
^^ may outlive borrowed value(«может пережить заимствованное значение») указывает на замыкание||. Rust не может пообещать, что замыкание закончит работу до того, какmainпойдёт дальше.`scores` is borrowed here(«scoresзаимствуется здесь») указывает на место, где замыкание используетscores. Посколькуprintln!нужно только прочитать вектор, замыкание захватило ссылку наscores, а не сам вектор.function requires argument type to outlive 'static(«функции нужно, чтобы тип аргумента жил не меньше'static») объясняет, в чём проблема:thread::spawnпринимает только замыкания, которые могут жить столько, сколько понадобится программе.'staticзначит «не заимствует ничего, что может исчезнуть».
Представь, что ты одалживаешь другу блокнот со словами «верни, когда закончишь», а сам вот-вот переезжаешь в другой город. Если друг работает медленно, блокноту некуда будет вернуться. Rust отказывается от такой сделки и просит тебя блокнот отдать.
Почему Rust так делает
Заголовок раздела «Почему Rust так делает»Замыкание захватывает переменные самым дешёвым способом, который работает. Если оно только читает переменную, то заимствует её через &. Это отлично подходит для замыканий, которые выполняются сразу, — например тех, что передаются в .map() или .filter(). Подробнее — в уроке Замыкания.
Новый поток — другое дело. Он может работать сколько угодно, и main его не ждёт, если не вызвать join. Если бы поток держал ссылку на scores, а main закончилась раньше, scores удалился бы, и поток читал бы освобождённую память. Это «висячая ссылка» — одна из самых частых ошибок в многопоточном коде на C и C++. Rust останавливает её при компиляции.
То же самое происходит, когда ты возвращаешь замыкание. Локальные переменные функции (и её параметры) удаляются, когда она возвращает управление. Замыкание, которое всё ещё их заимствует, указывало бы в пустоту.
Как исправить
Заголовок раздела «Как исправить»Исправление 1: добавь move (обычный ответ)
Заголовок раздела «Исправление 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 вернёт управление. С 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 не меняет трейт замыкания, который ты получаешь. Замыкание с move, которое только читает свои данные, по-прежнему Fn, и его можно вызывать много раз.
Исправление 2: сначала склонируй, если значение ещё нужно
Заголовок раздела «Исправление 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
Заголовок раздела «Исправление 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
Заголовок раздела «Исправление 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]Потоки с областью видимости (scoped threads) — самый чистый ответ на задачу «раздели работу, а потом используй результаты». С них начинается урок Общее состояние.
Где это обычно встречается
Заголовок раздела «Где это обычно встречается»Запуск потоков в цикле. Даже обычное число заимствуется, если замыкание только читает его:
for worker in 1..=3 { handles.push(thread::spawn(|| { println!("Рабочий {worker} запущен"); // ❌ заимствует `worker` }));}Исправление: thread::spawn(move || ...). worker — это i32, так что move просто копирует его в каждый поток.
Возврат замыкания в Box. Box::new(|| greeting.clone()) внутри функции, которая возвращает Box<dyn Fn() -> String>, заимствует локальную 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: missing lifetime specifier: появляется, когда пытаешься вернуть ссылку (или тип замыкания), которая что-то заимствует.
- Все ошибки компилятора: полный список страниц об ошибках.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Замыкание потока выводит name без move. Почему появляется E0373?
2. Ты добавил move, но main всё ещё нужно вывести вектор после запуска потока. Какое исправление самое простое?
3. Какой инструмент позволяет потокам заимствовать локальные переменные без move?
Результат: 0 / 3
Что почитать
Заголовок раздела «Что почитать»- E0373 — Rust error codes index (на английском): официальное объяснение этой ошибки с исправлением через
move. - Использование потоков для параллельного выполнения кода — книга «Язык программирования Rust» (перевод на русский): раздел о замыканиях
moveв потоках разбирает ровно эту ошибку. - Замыкания — книга «Язык программирования Rust» (перевод на русский): как замыкания захватывают переменные — по ссылке, по изменяемой ссылке или по значению.
- Ключевое слово
move— документация стандартной библиотеки (на английском): чтоmoveделает с захватами замыкания, с короткими примерами. std::thread::scope— документация стандартной библиотеки (на английском): потоки с областью видимости, которые могут безопасно заимствовать локальные данные.