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.

Rust Mit und ohne Semikolon
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:

Rust Tail expression
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:

Rust Semikolon-Falle
fn doppelt(x: i32) -> i32 {
    x * 2;          // Fehler — gibt () zurück, erwartet i32
}

rustc meldet:

Rust rustc-Diagnose
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 value

Die 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.

Rust Idiomatisch vs. Nicht-idiomatisch
// 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:

Rust Block als Initialisierer
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:

Rust if als Rückgabe
fn vorzeichen(x: i32) -> &'static str {
    if x > 0 {
        "positiv"
    } else if x < 0 {
        "negativ"
    } else {
        "null"
    }
}
Rust match als Rückgabe
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

Rust Asymmetrisch
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

Rust Unsichtbares ()
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

Rust Halb-fertig
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

Rust Validator
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

Rust Env-Lookup
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

Rust Match als ganzer 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

Rust Komplexer Init
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

Rust Builder mit tail expression
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

Rust Math-Helper
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

Rust Logger
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

/ Weiter

Zurück zu Funktionen

Zur Übersicht