if in Rust ist eine Expression, kein Statement — jeder Zweig liefert einen Wert, und das ganze Konstrukt kann direkt einer Bindung zugewiesen oder als Funktions-Rückgabe verwendet werden. Das macht den klassischen ternären Operator (cond ? a : b aus C/Java) überflüssig: Rust löst das mit normaler if-Syntax. Dieser Artikel zeigt den expression-orientierten Stil, geht durch die Typ-Regeln (alle Zweige gleicher Typ), behandelt die ASCII-Striktheit von Bool-Bedingungen und stellt typische Praxis-Patterns vor.
Grundform
fn main() {
let zahl = 5;
if zahl > 0 {
println!("positiv");
} else if zahl < 0 {
println!("negativ");
} else {
println!("null");
}
}So weit wie in jeder anderen Sprache — Statement-Form mit Side-Effects. Interessanter wird's, wenn if als Wert benutzt wird.
if als Expression
fn main() {
let zahl = 5;
let beschreibung = if zahl > 0 {
"positiv"
} else if zahl < 0 {
"negativ"
} else {
"null"
};
println!("{beschreibung}");
}Drei Dinge sind hier zentral:
- Kein Semikolon nach den String-Literalen in den Zweigen — sie sind die tail expression des jeweiligen Blocks und damit der Block-Wert.
- Alle Zweige müssen den gleichen Typ liefern — sonst Compile-Fehler.
else-Branch ist Pflicht, wenn derifeinen Wert produzieren soll.
Das ersetzt das fehlende Ternär:
// JavaScript/Java: let max = a > b ? a : b;
let a = 10;
let b = 20;
let max = if a > b { a } else { b };
let stunden = 14;
let begruessung = if stunden < 12 { "Guten Morgen" } else { "Guten Tag" };Typ-Konsistenz aller Zweige
fn main() {
let n = 5;
// let x = if n > 0 { "positiv" } else { 0 };
// Fehler E0308: expected `&str`, found integer
}Der Compiler verlangt, dass beide Branches denselben Typ haben — sonst wäre der Typ von x ambivalent.
Ausnahme: einer der Zweige liefert ! (Never-Type). Dann darf der andere einen beliebigen Typ liefern — ! coerciert sich passend:
fn parse_zahl(s: &str) -> i32 {
let n = if let Ok(v) = s.parse::<i32>() {
v
} else {
panic!("'{s}' ist keine Zahl") // ! coerciert zu i32
};
n
}Mehr zum Never-Type im Unit-und-Never-Artikel.
Bedingung muss bool sein
Anders als in JavaScript, Python oder C hat Rust kein Truthy/Falsy:
let n = 5;
// if n { ... } // Fehler — kein bool
// if "text" { ... } // Fehler
// if Some(1) { ... } // Fehler
if n != 0 { println!("nicht null"); }
if !s.is_empty() { println!("hat Inhalt"); }
if option.is_some() { println!("vorhanden"); }
# let s = "";
# let option: Option<i32> = None;Die Bedingung muss explizit bool sein. Wenn ein Zahlen-Vergleich gemeint ist, schreibst du n != 0. Das ist verbal aufwendiger, eliminiert aber eine ganze Klasse von subtilen Bugs (if user.id in JavaScript ist wahr für jede ID außer 0 — gilt das auch als „logged in"? Rust zwingt zur Klarheit).
else if als Kette
fn klassifiziere_alter(jahre: u8) -> &'static str {
if jahre < 18 {
"Minderjährig"
} else if jahre < 65 {
"Erwachsen"
} else {
"Senior"
}
}Das ist syntaktischer Zucker für ein verschachteltes if-else. Bei vielen Ästen wird match lesbarer — siehe Pattern-Matching-Kapitel.
if let — kompakter Pattern-Match
Wenn die Bedingung darin besteht, ein Pattern zu matchen (häufig Option/Result), gibt es eine spezielle Form: if let. Sie bekommt einen eigenen Artikel; hier nur ein Vorschau-Beispiel:
let maybe: Option<i32> = Some(42);
if let Some(n) = maybe {
println!("Wert: {n}");
}Praxis: if-Expressions in echtem Code
Default-Wert mit Override
fn timeout_ms(custom: Option<u64>) -> u64 {
if let Some(t) = custom { t } else { 5_000 } // 5s default
}
// Oder noch idiomatischer:
fn timeout_ms_v2(custom: Option<u64>) -> u64 {
custom.unwrap_or(5_000)
}Variante 2 ist idiomatischer, wenn Option::unwrap_or reicht — aber if let zeigt mehr Logik, falls noch andere Checks dazukommen.
HTTP-Statuscode klassifizieren
fn kategorie(status: u16) -> &'static str {
if status < 200 { "1xx Informational" }
else if status < 300 { "2xx Success" }
else if status < 400 { "3xx Redirect" }
else if status < 500 { "4xx Client Error" }
else { "5xx Server Error" }
}
fn main() {
assert_eq!(kategorie(200), "2xx Success");
assert_eq!(kategorie(404), "4xx Client Error");
}Klassische else-if-Kette für überschneidungsfreie Bereiche. Lesbar, ohne match mit Ranges.
Rabatt-Berechnung
struct Bestellung { artikel: u32, gesamt_cent: u32, ist_premium: bool }
fn rabatt_cent(b: &Bestellung) -> u32 {
// Hauptlogik in einer if-Expression
let prozent: u32 = if b.ist_premium && b.gesamt_cent >= 10_000 {
15
} else if b.gesamt_cent >= 5_000 {
10
} else if b.artikel >= 5 {
5
} else {
0
};
b.gesamt_cent * prozent / 100
}Statt mehrerer mut-Mutations sammelt eine einzige if-Expression den Rabatt-Prozentsatz. Lesbarer, weil der Wert sofort an die Bindung gebunden ist.
Theme-Auswahl beim Render
struct UserPrefs { dark_mode: bool }
fn render_hintergrund(prefs: &UserPrefs) -> &'static str {
if prefs.dark_mode { "#1a1a1a" } else { "#ffffff" }
}
fn render_text(prefs: &UserPrefs) -> &'static str {
if prefs.dark_mode { "#e0e0e0" } else { "#202020" }
}Einzeilige if-Expressions als Funktion-Body — kompakt, klar, kein return nötig.
Validierungs-Funktion mit frühem Ausstieg
struct Form { email: String, alter: u8, akzeptiert: bool }
fn validiere(f: &Form) -> Result<(), &'static str> {
if f.email.is_empty() { return Err("Email fehlt"); }
if !f.email.contains('@') { return Err("Email ungültig"); }
if f.alter < 18 { return Err("zu jung"); }
if !f.akzeptiert { return Err("AGB nicht akzeptiert"); }
Ok(())
}Mehrere unabhängige if-Checks mit Early-Return — sehr lesbarer Validator. Alternative: alle Conditions mit || verkettet — aber dann verliert man die spezifische Fehlermeldung.
Logging-Level entscheiden
struct Result_ { status: u16, latency_ms: u64 }
fn log_level(r: &Result_) -> &'static str {
if r.status >= 500 { "ERROR" }
else if r.status >= 400 || r.latency_ms > 5000 { "WARN" }
else if r.latency_ms > 1000 { "INFO" }
else { "DEBUG" }
}Bedingungen kombiniert mit || — die Logik liest sich wie eine Anforderungsbeschreibung.
Interessantes
Klammern um die Bedingung sind nicht nötig.
if (n > 0) ist gültig, aber unidiomatisch. Idiomatisch: if n > 0. Klammern sind nur Code-Smell, wenn sie die Lesbarkeit nicht messbar verbessern.
if ohne else als Expression ist verboten.
let x = if cond { 5 }; wäre für den false-Fall undefiniert — der Compiler verlangt einen else-Branch, sobald if als Wert genutzt wird. Statement-Form (if cond { do(); }) ohne else ist erlaubt.
Alle Branches müssen gleichen Typ liefern — oder !.
Eine häufige Stolperfalle: let x = if cond { 5 } else { "fünf" }; — Type-Mismatch. Ausnahme: panic!, return, loop {} haben Typ ! und coercen sich in jeden anderen Typ. So funktioniert let x = if cond { 5 } else { panic!() };.
Performance ist identisch zur Statement-Form.
Der Compiler optimiert if-Expressions zu denselben Maschinen-Instruktionen wie ein klassisches if/else mit Zuweisungen in jedem Branch. Es gibt keinen Laufzeit-Overhead für expression-orientierten Stil.
Bei mehr als 3 else-if besser match.
Eine else if-Kette mit 5+ Branches wird unübersichtlich. match mit Range-Patterns (0..=99 => ...) oder Guard-Patterns ist meist die bessere Wahl — siehe Pattern-Matching-Kapitel.
Clippy warnt vor if cond { true } else { false }.
Solche tautologischen Konstrukte sind direkt der Bedingung selbst. Clippy schlägt vor: cond statt der ganzen if-Expression. Gleichermaßen if !cond { false } else { true } → cond.
if-Expressions können in Closure-Bodies und Match-Armen leben.
Mit der Expression-Semantik passen if-Bedingungen überall hin, wo ein Wert erwartet wird — auch tief in Closure-Chains: iter.map(|x| if x > 0 { x } else { 0 }). Sehr idiomatisch für bedingtes Mapping.