Das vielleicht häufigste Stolperfeld für Rust-Einsteiger: das Semikolon ist keine Formalität, sondern eine Bedeutungs-Grenze. Eine Expression mit Semikolon wird zu einem Statement vom Typ (). Ohne Semikolon ist sie die tail expression eines Blocks — und damit dessen Rückgabewert. Dieselbe Regel gilt für Funktions-Bodies: das fehlende oder vergessene Semikolon entscheidet, ob die Funktion einen Wert oder () zurückgibt. Dieser Artikel erklärt die Regel präzise, zeigt, wo sie zuschlägt, und macht klar, wann return notwendig und wann es Stil-Konflikt ist.
Die Grundregel
In Rust gilt:
- Expression — produziert einen Wert:
2 + 3,if cond { a } else { b },{ let x = 5; x * 2 },vec[0]. - Statement — tut etwas, hat keinen Wert (technisch: Typ
()):let x = 5;,fn foo() {},2 + 3;(Expression mit Semikolon).
Das Semikolon macht aus einer Expression ein Statement.
fn main() {
let a = 5 + 3; // Statement (let), enthält Expression (5+3)
let b = { 5 + 3 }; // b: i32 — Block ist Expression, liefert 8
let c = { 5 + 3; }; // c: () — Semikolon macht aus 5+3 ein Statement
}Bei c wird die 5 + 3-Expression durch das Semikolon zu einer Anweisung; der Block hat keine tail expression mehr und gibt () zurück.
Die tail expression eines Funktions-Body
Der Body einer Funktion ist ein Block. Wenn die letzte Zeile eine Expression ohne Semikolon ist, wird sie zur Rückgabe:
fn doppelt(x: i32) -> i32 {
x * 2 // tail expression — Rückgabe
}
fn main() {
assert_eq!(doppelt(21), 42);
}Mit Semikolon wäre das ein Fehler:
fn doppelt(x: i32) -> i32 {
x * 2; // Fehler — gibt () zurück, erwartet i32
}rustc meldet:
error[E0308]: mismatched types
--> src/main.rs:1:24
|
1 | fn doppelt(x: i32) -> i32 {
| ------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail expression
2 | x * 2;
| - help: remove this semicolon to return this valueDie help-Zeile zeigt direkt die Lösung. rustc-Fehler sind in diesem Bereich besonders hilfreich.
return — explizit, oft optional
return ist eine Expression mit Typ ! (Never) — sie verlässt die Funktion sofort. Idiomatisches Rust nutzt return nur für frühen Ausstieg, nicht für die normale Rückgabe.
// Nicht idiomatisch (aber gültig):
fn doppelt_a(x: i32) -> i32 {
return x * 2;
}
// Idiomatisch:
fn doppelt_b(x: i32) -> i32 {
x * 2
}
// Sinnvoll mit return (früher Ausstieg):
fn betrag(x: i32) -> i32 {
if x >= 0 {
return x;
}
-x
}Clippy hat den Lint clippy::needless_return — der warnt vor unnötigen return-Statements am Ende einer Funktion.
Block-Expressions
Jeder { ... }-Block ist eine Expression. Damit lassen sich komplexe Initialisierungen kompakt schreiben:
fn main() {
let umfang = {
let radius = 5.0_f64;
let pi = std::f64::consts::PI;
2.0 * pi * radius // tail expression des Blocks
};
println!("{umfang:.2}"); // 31.42
}Die Hilfsbindungen radius und pi leben nur im Block — der äußere Scope ist sauber.
if und match als Expressions
Wie schon im Kontrollfluss-Kapitel: if und match sind Expressions. Sie können tail expression sein:
fn vorzeichen(x: i32) -> &'static str {
if x > 0 {
"positiv"
} else if x < 0 {
"negativ"
} else {
"null"
}
}fn beschreibe(n: i32) -> &'static str {
match n {
0 => "null",
1..=9 => "klein",
10..=99 => "mittel",
_ => "groß",
}
}Beide Funktionen brauchen kein return. Die if/match-Expression ist die tail expression — also die Rückgabe.
Häufige Stolperfallen
Vergessenes Semikolon im falschen Branch
fn klassifiziere(n: i32) -> &'static str {
if n > 0 {
"positiv"; // Semikolon! tail expression weg
} else {
"anders"
}
// Compiler-Fehler — beide Branches sollten gleichen Typ liefern,
// der if-Branch liefert () (durch Semikolon), der else-Branch &str
}Versehentliches Block-Statement statt Expression
fn berechne() -> i32 {
let x = {
let temp = 5;
temp * 2; // Semikolon — Block gibt () zurück
};
// let x: () — vermutlich nicht gewollt
// x // wäre () und keine Rückgabe
42
}Tail expression vergessen, return vergessen
fn first_or_default(v: &[i32]) -> i32 {
if v.is_empty() {
return 0;
}
v[0]; // Semikolon — Funktion gibt () zurück
// Compiler-Fehler: expected i32, found ()
}
// Fix: Semikolon weg
fn first_or_default_fix(v: &[i32]) -> i32 {
if v.is_empty() {
return 0;
}
v[0]
}Praxis: Expression-Stil im echten Code
Validator mit Early-Return + tail expression
pub fn email_normalisiert(s: &str) -> Result<String, &'static str> {
let getrimmt = s.trim();
if getrimmt.is_empty() {
return Err("leere Eingabe");
}
if !getrimmt.contains('@') {
return Err("kein @ gefunden");
}
Ok(getrimmt.to_lowercase()) // tail expression
}Zwei early-returns für die Fehler-Pfade, eine tail expression für den Erfolgs-Pfad. Klar lesbar.
Konfiguration aus Env mit Default
pub fn port_aus_env() -> u16 {
std::env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8080)
}Ein einziger Expression-Chain — keine Hilfsvariablen, keine Semikolons mittendrin. Die ganze Funktion ist eine Expression mit Rückgabe-Typ u16.
Klassifikator mit match als Body
pub enum Rolle { Admin, User, Gast }
pub fn rolle_aus_string(s: &str) -> Rolle {
match s.to_lowercase().as_str() {
"admin" | "owner" => Rolle::Admin,
"user" | "member" => Rolle::User,
_ => Rolle::Gast,
}
}Der gesamte Funktions-Body ist ein einzelnes match — tail expression. Keine Hilfs-Variablen, kein return.
Block-Initialisierung mit Hilfs-Bindungen
pub fn build_user(roh: &str) -> String {
let user_id = {
let bytes = roh.as_bytes();
let mut h: u64 = 5381;
for &b in bytes {
h = h.wrapping_mul(33).wrapping_add(b as u64);
}
format!("user-{h:x}")
};
user_id
}Die Hash-Berechnung lebt in einem inneren Block — bytes und h sind außerhalb nicht sichtbar. Das format! liefert die tail expression des Blocks, die an user_id gebunden wird.
Funktion ohne return mit Builder-Stil
pub struct LogConfig { level: String, format: String }
pub fn default_log_config() -> LogConfig {
LogConfig {
level: "INFO".to_string(),
format: "{date} [{level}] {msg}".to_string(),
}
}Der ganze Struct-Konstruktor ist die tail expression — kein let cfg = ...; return cfg;.
Mathematische Hilfsfunktion
pub fn distanz(a: (f64, f64), b: (f64, f64)) -> f64 {
let dx = a.0 - b.0;
let dy = a.1 - b.1;
(dx * dx + dy * dy).sqrt()
}Zwei Statements (let-Bindungen mit Semikolon) plus eine tail expression. Klassisches Pattern.
Side-Effect-Funktion ohne Rückgabe
pub fn log_event(level: &str, msg: &str) {
let ts = std::time::SystemTime::now();
eprintln!("[{level}] {ts:?} {msg}");
}Kein Rückgabe-Typ → implizit (). Die letzte Anweisung darf Semikolon haben (oder fehlen — beide ergeben ()).
Häufige Stolperfallen
Letzte Zeile mit Semikolon = ().
Der Klassiker. fn quadrat(x: i32) -> i32 { x * x; } ist ein Compile-Fehler. rustc zeigt unmittelbar den Fix mit „remove this semicolon" in der help-Zeile. Wer den Fehler einmal gesehen hat, vergisst die Regel selten wieder.
return am Funktions-Ende ist ein Code-Smell.
Clippy warnt mit clippy::needless_return. Idiomatic Rust nutzt return nur für früheren Ausstieg, nicht für die normale Rückgabe. Die tail expression ist die idiomatische Form.
Beide if-Branches müssen gleichen Typ liefern.
Wenn ein Branch eine tail expression hat und der andere mit Semikolon endet, liefert der eine seinen Typ, der andere () — Type-Mismatch. Lösung: Semikolons in beiden Branches konsistent setzen.
Block-Initialisierung mit innerem Scope.
let x = { let helper = ...; berechnung }; lässt helper nach dem Block verschwinden. Der äußere Scope bleibt sauber — eines der elegantesten Patterns für „temporäre Hilfsdaten für eine Berechnung".
Letzte Expression im Block kann jede Expression sein.
match, if, loop, ein anderer Block — alles kann tail expression sein. Damit lassen sich Funktions-Bodies sehr kompakt halten.
Macros wie println! sind Expressions vom Typ ().
println!("...") ist eine Expression, deren Wert () ist. Mit Semikolon wird sie zur Anweisung; ohne Semikolon ist sie tail expression eines Blocks mit Typ () — was nur in fn ... -> () (also ohne explizite Rückgabe) als Rückgabe taugt.
?-Operator ist eine Expression.
let x = riskant()?; — der ? ist Teil einer Expression, die entweder den Wert liefert oder die Funktion mit Err verlässt. Damit lässt sich ? direkt in Method-Chains nutzen: let n: u32 = "42".parse::<u32>()? * 2;.
Die Regel gilt rekursiv für alle Blocks.
Block in Block in Block — jeder hat seine eigene tail expression. let x = { let y = { 5 }; y * 2 }; ist gültig — 5 ist tail von innerem Block (gibt 5), y * 2 ist tail von äußerem Block (gibt 10).
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Statements and Expressions
- Rust Reference – Statements
- Rust Reference – Expressions
- Clippy –
needless_returnLint - rustc Error E0308