Перейти к содержимому

Ошибка 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:?}"); // ❌ только заимствует scores
7 | | });
| |______^
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).

Прочитай пометки сверху вниз:

  1. ^^ may outlive borrowed value («может пережить заимствованное значение») указывает на замыкание ||. Rust не может пообещать, что замыкание закончит работу до того, как main пойдёт дальше.
  2. `scores` is borrowed here («scores заимствуется здесь») указывает на место, где замыкание использует scores. Поскольку println! нужно только прочитать вектор, замыкание захватило ссылку на scores, а не сам вектор.
  3. function requires argument type to outlive 'static («функции нужно, чтобы тип аргумента жил не меньше 'static») объясняет, в чём проблема: thread::spawn принимает только замыкания, которые могут жить столько, сколько понадобится программе. 'static значит «не заимствует ничего, что может исчезнуть».

Представь, что ты одалживаешь другу блокнот со словами «верни, когда закончишь», а сам вот-вот переезжаешь в другой город. Если друг работает медленно, блокноту некуда будет вернуться. Rust отказывается от такой сделки и просит тебя блокнот отдать.

Замыкание захватывает переменные самым дешёвым способом, который работает. Если оно только читает переменную, то заимствует её через &. Это отлично подходит для замыканий, которые выполняются сразу, — например тех, что передаются в .map() или .filter(). Подробнее — в уроке Замыкания.

Новый поток — другое дело. Он может работать сколько угодно, и main его не ждёт, если не вызвать join. Если бы поток держал ссылку на scores, а main закончилась раньше, scores удалился бы, и поток читал бы освобождённую память. Это «висячая ссылка» — одна из самых частых ошибок в многопоточном коде на C и C++. Rust останавливает её при компиляции.

То же самое происходит, когда ты возвращаешь замыкание. Локальные переменные функции (и её параметры) удаляются, когда она возвращает управление. Замыкание, которое всё ещё их заимствует, указывало бы в пустоту.

Выбирай этот вариант почти всегда. Именно это предлагает 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));
}

Вывод:

6
11
▶ Попробуй сам

move не меняет трейт замыкания, который ты получаешь. Замыкание с 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