loop ist die einfachste — und gleichzeitig leistungsfähigste — Schleifen-Form in Rust. Ohne Bedingung am Kopf, ohne Iterator, läuft sie bis sie per break oder return verlassen wird. Das Besondere: nur loop erlaubt break <wert>, also einen Wert beim Verlassen zurückzugeben. Damit ist loop die richtige Wahl für Polling-Loops, Retry-Logik, Event-Verarbeitung und überhaupt alle Pattern, bei denen die Abbruch-Bedingung erst im Body sichtbar wird.
Grundform
fn main() {
let mut zaehler = 0;
loop {
zaehler += 1;
if zaehler == 5 {
break;
}
}
println!("Zähler: {zaehler}"); // 5
}loop ohne break läuft für immer — der Compiler gibt ihm in diesem Fall den Rückgabetyp ! (Never). Eine loop-Schleife, die nirgends break, return oder panic! enthält, terminiert nicht.
break mit Wert
Das macht loop einzigartig:
fn main() {
let mut zaehler = 0;
let resultat = loop {
zaehler += 1;
if zaehler == 10 {
break zaehler * 2;
}
};
println!("{resultat}"); // 20
}break <wert> macht aus der ganzen loop-Expression einen Wert. Damit kann eine Schleife direkt einer Bindung zugewiesen werden — kein Hilfs-mut außerhalb nötig.
Wichtig: Nur loop hat diese Form. while und for haben sie nicht — ihre Rückgabe ist immer ().
Typ aller break-Wert muss konsistent sein
// ok — alle break liefern i32
let n = loop {
if cond_a() { break 1; }
if cond_b() { break 2; }
};
// Fehler — Typ-Konflikt zwischen Branches
// let x = loop {
// if cond { break 5; }
// break "fünf";
// };
# fn cond_a() -> bool { true }
# fn cond_b() -> bool { false }
# let cond = true;Wann loop, wann while oder for?
| Situation | Wahl |
|---|---|
| Bekannte Sequenz / Iterator | for x in ... |
| Bedingung am Schleifen-Anfang prüfbar | while cond |
| Bedingung erst im Body sichtbar (z. B. nach Berechnung) | loop mit break |
| Schleife muss einen Wert zurückgeben | loop mit break <wert> |
| Endlos-Service (Daemon) | loop ohne break |
Klassisches Beispiel für „Bedingung erst im Body sichtbar": Retry-Logik, bei der das Ergebnis erst nach dem Versuch klar ist.
loop ohne break — das Never-Type
Eine loop-Schleife ohne Ausstieg hat Typ !:
fn server() -> ! {
loop {
// Handle incoming requests
}
}Da der Compiler weiß: hier wird nie zurückgekehrt, darf der Rückgabetyp ! sein (siehe Unit-und-Never-Artikel).
Praxis: loop in echtem Code
Retry mit exponentiellem Backoff
use std::thread::sleep;
use std::time::Duration;
fn lade_mit_retry(url: &str) -> Result<String, &'static str> {
let mut versuch = 0;
let max_versuche = 5;
loop {
match hole(url) {
Ok(body) => break Ok(body), // Erfolg: loop liefert Ok
Err(e) if versuch >= max_versuche => break Err(e), // zu viele Versuche
Err(_) => {
versuch += 1;
let wartezeit = Duration::from_millis(100 * 2u64.pow(versuch));
sleep(wartezeit);
}
}
}
}
# fn hole(_: &str) -> Result<String, &'static str> { Err("fail") }loop ist hier ideal, weil das Ergebnis erst nach dem Versuch bekannt ist. break Ok(body) und break Err(e) liefern verschiedene Result-Werte aus derselben Schleife.
User-Input bis korrekt
use std::io::{self, BufRead};
fn frage_zahl(prompt: &str) -> i32 {
let stdin = io::stdin();
loop {
println!("{prompt}");
let mut line = String::new();
if stdin.lock().read_line(&mut line).is_err() {
continue;
}
match line.trim().parse() {
Ok(n) => break n,
Err(_) => println!("Keine gültige Zahl, nochmal."),
}
}
}Klassisches CLI-Pattern: bis der User eine gültige Eingabe macht, weiterfragen. Das break n gibt den geparsten Wert direkt aus der Funktion zurück.
Polling auf Channel
use std::sync::mpsc::Receiver;
use std::time::Duration;
enum Cmd { Verarbeite(String), Beenden }
fn worker(rx: Receiver<Cmd>) {
loop {
match rx.recv_timeout(Duration::from_secs(1)) {
Ok(Cmd::Verarbeite(daten)) => {
println!("Verarbeite: {daten}");
}
Ok(Cmd::Beenden) => {
println!("Worker beendet.");
break;
}
Err(_) => {
// Timeout — Heartbeat, weiter
}
}
}
}Worker-Threads laufen oft in loops, die per Beendigungs-Signal aus einem Channel terminieren. Der Cmd::Beenden-Branch ist die einzige break-Stelle — alles andere lässt den Loop weiterlaufen.
Binäre Suche „von Hand"
fn finde<F: Fn(i64) -> bool>(min: i64, max: i64, ok: F) -> i64 {
let mut lo = min;
let mut hi = max;
loop {
if lo + 1 >= hi { break lo; }
let mitte = (lo + hi) / 2;
if ok(mitte) {
lo = mitte;
} else {
hi = mitte;
}
}
}
fn main() {
// Finde das größte i mit i*i <= 100
let n = finde(0, 1000, |i| i * i <= 100);
assert_eq!(n, 10);
}Hier ist break lo die Wert-Rückgabe der Schleife. Eleganter als ein Hilfs-mut-Result-Bindung außerhalb.
Event-Loop in einer Game-/UI-Anwendung
struct State { running: bool, ticks: u64 }
fn run_game() {
let mut state = State { running: true, ticks: 0 };
while state.running {
// ... Event-Handling ...
state.ticks += 1;
if state.ticks > 100 { state.running = false; }
}
// Oder mit loop und break:
}
fn run_game_v2() {
let mut state = State { running: true, ticks: 0 };
loop {
if !state.running { break; }
state.ticks += 1;
if state.ticks > 100 { state.running = false; }
}
}Beide Stile sind gültig. while ist hier vielleicht einen Tick lesbarer — solche Entscheidungen folgen oft persönlicher oder Team-Konvention.
Worker-Pool mit Job-Annahme
use std::sync::Arc;
use std::sync::Mutex;
struct JobQueue {
offen: Mutex<Vec<String>>,
}
fn worker(queue: Arc<JobQueue>) {
loop {
let job = {
let mut offen = queue.offen.lock().unwrap();
if offen.is_empty() {
break;
}
offen.remove(0)
};
println!("Verarbeite: {job}");
}
}loop mit innerem Scope-Block: erst Lock holen, prüfen, Job nehmen, Lock loslassen, dann verarbeiten. Sehr typisches Concurrency-Pattern — Mutex-Guard lebt nur im inneren Block, danach kann ohne Lock weitergearbeitet werden.
Besonderheiten
loop ist die einzige Schleife mit break .
while und for haben immer Typ () — sie können einen Wert nicht zurückgeben. Wenn du also „Schleife mit Resultat-Wert" willst, ist loop die richtige Wahl.
loop ohne break hat Typ !.
Eine wirklich endlose Schleife terminiert nie — der Compiler gibt der Expression Typ !. Damit lässt sich ein endloser loop als Funktions-Body einer -> !-Funktion einsetzen (Server-Daemons, Embedded-Main-Loops).
continue in loop springt zum Schleifen-Anfang.
Im Gegensatz zu while oder for gibt es keine Bedingung am Anfang — continue springt direkt wieder zur ersten Anweisung im Body. Beispiel: in Retry-Schleifen, wo das Backoff-Sleep am Ende des Body steht, ist continue der Weg, dieses Sleep zu überspringen.
break ohne Wert in einer Wert-liefernden Schleife — Compile-Fehler.
Wenn andere Stellen break <wert> rufen, müssen alle break einen Wert haben (oder durch Coercion über ! passen). Ein nacktes break; in einer ansonsten Wert-liefernden Schleife produziert einen Type-Mismatch.
loop wird oft mit match kombiniert.
Klassisches Pattern: loop { match recv() { Ok(x) => break x, Err(_) => continue } }. Die match-Arme entscheiden: weiter (continue) oder mit Wert raus (break x). Sehr lesbar für Polling-Logik.
Labels erweitern loop auf verschachtelte Strukturen.
Bei verschachtelten Schleifen kannst du mit 'name: loop { ... break 'name <wert>; } aus einer äußeren Schleife mit Wert ausbrechen. Mehr im break/continue-Artikel.
loop { } ist nicht „busy wait“-frei.
Ein loop ohne sleep, recv oder I/O verbrennt eine ganze CPU-Kerne. In Concurrency-Code immer mit Backoff oder Blocking-Operationen kombinieren. Tools wie tokio::select! lösen das in async-Code idiomatisch.