Salta ai contenuti

Errore Rust E0373: closure may outlive the current function (usa move)

E0373 significa che una closure prende in prestito una variabile locale, ma potrebbe continuare a essere eseguita (o a esistere) dopo che la funzione proprietaria di quella variabile è terminata. Di solito lo incontri la prima volta che chiami thread::spawn o che restituisci una closure da una funzione. La buona notizia: il compilatore ti dice già la soluzione, e quasi sempre è una sola parola, move. Questa pagina ti mostra prima la soluzione e poi il perché. Per gli altri errori di questa serie, vedi Errori del compilatore 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:?}"); // ❌ only borrows 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:?}"); // ❌ only borrows 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 || {
| ++++

Il compilatore dice: «la closure potrebbe vivere più a lungo della funzione corrente, ma prende in prestito scores, che appartiene alla funzione corrente». Questo è il programma che lo produce:

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(|| {
println!("Scores: {scores:?}"); // ❌ only borrows scores
});
handle.join().unwrap();
}

Con il codice async vedrai lo stesso codice con parole leggermente diverse: «async block may outlive the current function, but it borrows user_id». È lo stesso errore, con la stessa soluzione (async move).

Leggi le etichette dall’alto verso il basso:

  1. ^^ may outlive borrowed value indica la closure ||. Rust non può garantire che la closure abbia finito prima che main vada avanti.
  2. `scores` is borrowed here indica il punto in cui la closure usa scores. Visto che a println! basta leggerlo, la closure ha catturato un riferimento a scores, non il vettore stesso.
  3. function requires argument type to outlive 'static spiega perché è un problema: thread::spawn accetta solo closure che possono vivere per tutto il tempo che serve al programma. 'static vuol dire «non prende in prestito niente che possa sparire».

Immagina di prestare il tuo quaderno a un amico dicendogli «riportamelo quando hai finito», mentre stai per trasferirti in un’altra città. Se il tuo amico è lento, il quaderno non avrà più una casa in cui tornare. Rust rifiuta questo accordo e ti chiede invece di regalare il quaderno.

Una closure cattura le variabili nel modo più economico che funziona. Se legge soltanto una variabile, la prende in prestito con &. È perfetto per le closure che vengono eseguite subito, come quelle che passi a .map() o .filter(). Trovi di più nella pagina Closure.

Un nuovo thread è diverso. Può durare un tempo qualsiasi, e main non lo aspetta a meno che tu non chiami join. Se il thread tenesse un riferimento a scores e main finisse per primo, scores verrebbe eliminato e il thread leggerebbe memoria già liberata. È un «riferimento pendente» (dangling reference), uno dei bug più comuni nel codice multithread in C e C++. Rust lo blocca in fase di compilazione.

Lo stesso succede quando restituisci una closure. Le variabili locali della funzione (e i suoi parametri) vengono eliminate quando la funzione termina. Una closure che le prende ancora in prestito punterebbe al nulla.

Sceglila quasi sempre. È esattamente ciò che suggerisce rustc. move dice alla closure di prendere l’ownership (possesso) delle variabili che usa, così viaggiano insieme a lei:

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(move || {
let total: i32 = scores.iter().sum();
println!("Punteggi: {scores:?}, totale {total}");
});
handle.join().unwrap();
}

Output:

Punteggi: [90, 85, 77], totale 252
▶ Provalo tu

La stessa soluzione funziona per una funzione che restituisce una closure. Senza move, |x| x + n prende in prestito il parametro n, che sparisce appena make_adder termina. Con move, ogni closure si tiene la propria copia di 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));
}

Output:

6
11
▶ Provalo tu

move non cambia il trait della closure che ottieni. Una closure move che legge soltanto i suoi dati resta Fn e si può chiamare tutte le volte che vuoi.

Soluzione 2: Clona prima, se il valore ti serve ancora

Sezione intitolata “Soluzione 2: Clona prima, se il valore ti serve ancora”

Dopo move, la variabile appartiene alla closure. Se main la usa più avanti, ottieni E0382 («borrow of moved value»), e rustc suggerisce di clonare il valore prima di spostarlo. Fai una copia per la closure e tieniti l’originale:

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!("Il thread vede {} ospiti", guests_for_thread.len());
});
handle.join().unwrap();
println!("main ha ancora: {guests:?}");
}

Output:

Il thread vede 2 ospiti
main ha ancora: ["Ana", "Ben"]
▶ Provalo tu

Clonare una lista piccola costa poco e va benissimo. Se i dati sono grandi, o servono a molti thread, usa Arc (Soluzione 3), così condividono tutti un’unica copia.

Scegli questa quando più thread devono leggere gli stessi dati. Arc («atomically reference counted», conteggio dei riferimenti atomico) permette a molti proprietari di condividere un solo valore. Arc::clone copia solo un puntatore e incrementa un contatore; i dati in sé non vengono copiati:

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); // un handle per ogni thread
handles.push(thread::spawn(move || {
let total: f64 = prices.iter().sum();
total * (1.0 - discount)
}));
}
for handle in handles {
println!("Totale: {:.2}", handle.join().unwrap());
}
println!("Prezzi: {prices:?}");
}

Output:

Totale: 10.00
Totale: 9.00
Totale: 5.00
Prezzi: [4.5, 2.0, 3.5]
▶ Provalo tu

Da solo, Arc permette di condividere in sola lettura. Se i thread devono anche cambiare i dati, avvolgili in Arc<Mutex<T>>. Vedi Concorrenza e Stato condiviso.

Soluzione 4: Prendi in prestito in sicurezza con std::thread::scope

Sezione intitolata “Soluzione 4: Prendi in prestito in sicurezza con std::thread::scope”

Scegli questa quando ai thread servono i dati solo per un lavoro breve e preferisci prendere in prestito invece di spostare o clonare. I thread avviati dentro thread::scope finiscono di sicuro prima che scope ritorni, quindi prendere in prestito variabili locali è sicuro e move non serve:

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())
}); // qui entrambi i thread hanno finito
println!("{a} + {b} = {}", a + b);
println!("Ancora nostri: {scores:?}");
}

Output:

175 + 141 = 316
Ancora nostri: [90, 85, 77, 64]
▶ Provalo tu

I thread con scope sono la risposta più pulita per «dividi il lavoro, poi usa i risultati». La lezione Stato condiviso parte proprio da loro.

Avviare thread in un ciclo. Anche un semplice numero viene preso in prestito, se la closure lo legge soltanto:

for worker in 1..=3 {
handles.push(thread::spawn(|| {
println!("Worker {worker} started"); // ❌ borrows `worker`
}));
}

Soluzione: thread::spawn(move || ...). worker è un i32, quindi move lo copia semplicemente in ogni thread.

Restituire una closure in un Box. Box::new(|| greeting.clone()) dentro una funzione che restituisce Box<dyn Fn() -> String> prende in prestito la variabile locale greeting, e rustc aggiunge note: closure is returned here. Soluzione: Box::new(move || greeting.clone()). Trovi di più in Fn, FnMut e FnOnce.

Task asincroni (Tokio e simili). tokio::spawn funziona come thread::spawn: il task può durare molto oltre la fine della funzione corrente, quindi ha bisogno di dati 'static. Se scrivi tokio::spawn(async { ... }) e dentro usi una variabile locale, ottieni «async block may outlive the current function». Soluzione: tokio::spawn(async move { ... }), e clona (o fai Arc::clone di) tutto quello che ti serve ancora fuori dal task. Vedi Async/await.

Verifica veloce

1. La closure di un thread stampa name senza usare move. Perché ottieni E0373?

2. Aggiungi move, ma main deve ancora stampare il vettore dopo aver avviato il thread. Qual è la soluzione più semplice?

3. Quale strumento permette ai thread di prendere in prestito variabili locali senza move?

Punteggio: 0 / 3