Rust-Fehler E0373: closure may outlive the current function (move fehlt)
E0373 bedeutet: Ein Closure (eine anonyme Funktion) leiht sich eine lokale Variable, aber das Closure könnte noch laufen (oder noch existieren), wenn die Funktion, der diese Variable gehört, schon fertig ist. Meistens triffst du den Fehler, wenn du zum ersten Mal thread::spawn aufrufst oder ein Closure aus einer Funktion zurückgibst. Die gute Nachricht: Der Compiler verrät dir die Lösung schon selbst, und sie ist fast immer ein einziges Wort – move. Diese Seite zeigt dir zuerst die Lösung und erklärt dann, warum. Die anderen Fehler dieser Reihe findest du unter Rust-Compilerfehler.
Die Fehlermeldung
Abschnitt betitelt „Die Fehlermeldung“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:?}"); // ❌ leiht sich scores nur aus | ------ `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:?}"); // ❌ leiht sich scores nur aus7 | | }); | |______^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 || { | ++++Auf Deutsch heißt das ungefähr: „Das Closure lebt vielleicht länger als die aktuelle Funktion, aber es leiht sich scores, und scores gehört der aktuellen Funktion.“ Dieses Programm erzeugt die Meldung:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(|| { println!("Scores: {scores:?}"); // ❌ leiht sich scores nur aus }); handle.join().unwrap();}Bei async-Code siehst du denselben Fehlercode mit leicht anderen Worten: „async block may outlive the current function, but it borrows user_id“. Es ist derselbe Fehler mit derselben Lösung (async move).
Was sie bedeutet
Abschnitt betitelt „Was sie bedeutet“Lies die Labels von oben nach unten:
^^ may outlive borrowed valuezeigt auf das Closure||. Rust kann nicht versprechen, dass das Closure fertig ist, bevormainweitermacht.`scores` is borrowed herezeigt auf die Stelle, an der das Closurescoresbenutzt. Weilprintln!den Vektor nur lesen muss, hat das Closure eine Referenz aufscoreseingefangen, nicht den Vektor selbst.function requires argument type to outlive 'staticerklärt, warum das ein Problem ist:thread::spawnnimmt nur Closures an, die so lange leben können, wie das Programm sie braucht.'staticheißt hier: „leiht sich nichts, was verschwinden könnte“.
Stell dir vor, du leihst einem Freund dein Notizbuch und sagst „bring es zurück, wenn du fertig bist“ – während du gerade in eine andere Stadt umziehst. Wenn dein Freund langsam arbeitet, hat das Notizbuch kein Zuhause mehr, zu dem es zurückkehren kann. Rust lehnt diesen Deal ab und bittet dich, das Notizbuch stattdessen zu verschenken.
Warum Rust das macht
Abschnitt betitelt „Warum Rust das macht“Ein Closure fängt Variablen auf die billigste Art ein, die funktioniert. Liest es eine Variable nur, leiht es sie sich mit &. Das ist ideal für Closures, die sofort laufen, etwa die, die du an .map() oder .filter() übergibst. Mehr dazu findest du auf der Seite Closures.
Ein neuer Thread ist anders. Er kann beliebig lange laufen, und main wartet nicht auf ihn, solange du nicht join aufrufst. Hätte der Thread eine Referenz auf scores, und main wäre zuerst fertig, würde scores freigegeben, und der Thread würde freigegebenen Speicher lesen. Das ist eine „hängende Referenz“ (Dangling Reference), einer der häufigsten Fehler in Thread-Code in C und C++. Rust verhindert ihn schon beim Kompilieren.
Dasselbe passiert, wenn du ein Closure zurückgibst. Die lokalen Variablen der Funktion (und ihre Parameter) werden beim Zurückkehren freigegeben. Ein Closure, das sie sich noch leiht, würde ins Leere zeigen.
So behebst du den Fehler
Abschnitt betitelt „So behebst du den Fehler“Lösung 1: move hinzufügen (die übliche Antwort)
Abschnitt betitelt „Lösung 1: move hinzufügen (die übliche Antwort)“Nimm diese Lösung fast immer. Genau das schlägt rustc vor. move sagt dem Closure, dass es die Variablen, die es benutzt, besitzen soll (Ownership). So reisen sie mit ihm:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(move || { let total: i32 = scores.iter().sum(); println!("Punkte: {scores:?}, Summe {total}"); }); handle.join().unwrap();}Ausgabe:
Punkte: [90, 85, 77], Summe 252Dieselbe Lösung funktioniert für eine Funktion, die ein Closure zurückgibt. Ohne move leiht sich |x| x + n den Parameter n – und der ist weg, sobald make_adder zurückkehrt. Mit move behält jedes Closure seine eigene Kopie von 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));}Ausgabe:
611move ändert nicht, welchen Closure-Trait du bekommst. Ein move-Closure, das seine Daten nur liest, ist weiterhin Fn und lässt sich beliebig oft aufrufen.
Lösung 2: Vorher klonen, wenn du den Wert noch brauchst
Abschnitt betitelt „Lösung 2: Vorher klonen, wenn du den Wert noch brauchst“Nach move gehört die Variable dem Closure. Benutzt main sie danach noch, bekommst du E0382 („borrow of moved value“), und rustc schlägt vor, den Wert vor dem Verschieben zu klonen. Mach eine Kopie für das Closure und behalte das Original:
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!("Der Thread sieht {} Gäste", guests_for_thread.len()); }); handle.join().unwrap();
println!("main hat noch: {guests:?}");}Ausgabe:
Der Thread sieht 2 Gästemain hat noch: ["Ana", "Ben"]Eine kleine Liste zu klonen ist billig und völlig in Ordnung. Sind die Daten groß, oder brauchen viele Threads sie, nimm Arc (Lösung 3). Dann teilen sich alle eine einzige Kopie.
Lösung 3: Mit Arc zwischen mehreren Threads teilen
Abschnitt betitelt „Lösung 3: Mit Arc zwischen mehreren Threads teilen“Nimm diese Lösung, wenn mehrere Threads dieselben Daten lesen müssen. Arc („atomically reference counted“, atomar referenzgezählt) lässt viele Besitzer einen Wert teilen. Arc::clone kopiert nur einen Zeiger und erhöht einen Zähler; die Daten selbst werden nicht kopiert:
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); // ein Handle pro Thread handles.push(thread::spawn(move || { let total: f64 = prices.iter().sum(); total * (1.0 - discount) })); }
for handle in handles { println!("Summe: {:.2}", handle.join().unwrap()); } println!("Preise: {prices:?}");}Ausgabe:
Summe: 10.00Summe: 9.00Summe: 5.00Preise: [4.5, 2.0, 3.5]Arc allein erlaubt nur gemeinsames Lesen. Müssen die Threads die Daten auch ändern, verpack sie als Arc<Mutex<T>>. Siehe Nebenläufigkeit und Geteilter Zustand.
Lösung 4: Sicher ausleihen mit std::thread::scope
Abschnitt betitelt „Lösung 4: Sicher ausleihen mit std::thread::scope“Nimm diese Lösung, wenn die Threads die Daten nur für eine kurze Aufgabe brauchen und du lieber ausleihen als verschieben oder klonen willst. Threads, die innerhalb von thread::scope gestartet werden, sind garantiert fertig, bevor scope zurückkehrt. Lokale Variablen auszuleihen ist dann sicher, und du brauchst kein 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()) }); // hier sind beide Threads fertig
println!("{a} + {b} = {}", a + b); println!("Gehört noch uns: {scores:?}");}Ausgabe:
175 + 141 = 316Gehört noch uns: [90, 85, 77, 64]Scoped Threads sind die sauberste Antwort für „Arbeit aufteilen, dann die Ergebnisse benutzen“. Die Lektion Geteilter Zustand beginnt mit ihnen.
Wo du ihn oft siehst
Abschnitt betitelt „Wo du ihn oft siehst“Threads in einer Schleife starten. Sogar eine einfache Zahl wird ausgeliehen, wenn das Closure sie nur liest:
for worker in 1..=3 { handles.push(thread::spawn(|| { println!("Worker {worker} started"); // ❌ leiht sich `worker` aus }));}Lösung: thread::spawn(move || ...). worker ist ein i32, also kopiert move den Wert einfach in jeden Thread.
Ein Closure in einer Box zurückgeben. Box::new(|| greeting.clone()) in einer Funktion, die Box<dyn Fn() -> String> zurückgibt, leiht sich die lokale Variable greeting, und rustc ergänzt note: closure is returned here (das Closure wird hier zurückgegeben). Lösung: Box::new(move || greeting.clone()). Mehr dazu in Fn, FnMut und FnOnce.
Async-Tasks (Tokio und Co.). tokio::spawn funktioniert wie thread::spawn: Der Task läuft vielleicht noch lange, nachdem die aktuelle Funktion zurückgekehrt ist, also braucht er 'static-Daten. Schreibst du tokio::spawn(async { ... }) und benutzt darin eine lokale Variable, bekommst du „async block may outlive the current function“. Lösung: tokio::spawn(async move { ... }), und alles, was du außerhalb des Tasks noch brauchst, vorher mit clone oder Arc::clone kopieren. Siehe Async/await.
Verwandte Fehler
Abschnitt betitelt „Verwandte Fehler“- E0382: borrow of moved value: taucht direkt auf, nachdem du
movehinzugefügt hast und die Variable außerhalb des Closures noch einmal benutzt. Klone sie vorher (Lösung 2) oder nimmArc(Lösung 3). - E0597: borrowed value does not live long enough: ein naher Verwandter. Eine Referenz wird noch benutzt, nachdem der Wert, auf den sie zeigt, schon freigegeben wurde.
- E0515: cannot return reference to local variable: dasselbe Problem „weg, sobald die Funktion zurückkehrt“, nur mit einer einfachen Referenz statt einem Closure.
- E0106: missing lifetime specifier: erscheint, wenn du eine Referenz (oder einen Closure-Typ) zurückgeben willst, die sich etwas leiht.
- Alle Compilerfehler: die vollständige Liste der Fehlerseiten.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Ein Thread-Closure gibt name aus, ohne move zu benutzen. Warum bekommst du E0373?
2. Du fügst move hinzu, aber main muss den Vektor nach dem Start des Threads noch ausgeben. Was ist die einfachste Lösung?
3. Mit welchem Werkzeug können Threads lokale Variablen ohne move ausleihen?
Punkte: 0 / 3
Weiterlesen
Abschnitt betitelt „Weiterlesen“- E0373 – Rust error codes index (auf Englisch): die offizielle Erklärung dieses Fehlers, mit der
move-Lösung. - Verwenden von move-Closures mit Threads – Das Rust-Buch: Dieser Abschnitt geht genau diesen Fehler Schritt für Schritt durch.
- Closures – Das Rust-Buch: wie Closures Variablen per Referenz, per veränderbarer Referenz oder per Wert einfangen.
- Schlüsselwort
move– Doku der Standardbibliothek (auf Englisch): wasmovemit den eingefangenen Variablen eines Closures macht, mit kurzen Beispielen. std::thread::scope– Doku der Standardbibliothek (auf Englisch): Scoped Threads, die sich lokale Daten sicher ausleihen können.