Wie Werte in eine Funktion hinein- und wieder herauskommen, ist in Rust präziser geregelt als in den meisten anderen Sprachen. Es gibt drei Pass-Modi für Parameter — by-value (Default, mit Move-Semantik), by-shared-reference (&T, lesend), by-mutable-reference (&mut T, schreibend) — und drei klassische Rückgabe-Patterns: Einzelwert, Tupel und eigene Struct/Enum-Typen. Dieser Artikel zeigt jede Variante mit konkreten Auswirkungen auf Ownership, Borrow Checker und API-Design.

Pass-by-Value — der Default

Wenn du einen Parameter ohne & schreibst, wird der Wert per Move (bei nicht-Copy-Typen) oder per Copy (bei Copy-Typen) übergeben.

Rust Copy-Typen — Kopie
fn doppelt(x: i32) -> i32 {
    x * 2
}

fn main() {
    let n = 5;
    let d = doppelt(n);
    println!("{n} {d}");           // 5 10 — n weiterhin nutzbar
}

i32 ist Copy, also wird der Wert kopiert. n ist nach dem Aufruf unverändert verfügbar.

Rust Move-Typen — verbraucht
fn drucken(s: String) {
    println!("{s}");
}

fn main() {
    let text = String::from("Hallo");
    drucken(text);
    // println!("{text}");          // Fehler — text wurde gemoved
}

String ist nicht Copy. Bei drucken(text) wird text in die Funktion gemoved — danach ist es im Aufrufer-Scope nicht mehr verfügbar.

Mehr zu Move/Copy im Ownership-Kapitel.

Pass-by-Reference — &T

Mit &T übergibst du eine geliehene Referenz. Die Funktion kann lesen, nicht schreiben — und der Aufrufer behält den Wert.

Rust &String / &str
fn drucken(s: &str) {
    println!("{s}");
}

fn main() {
    let text = String::from("Hallo");
    drucken(&text);            // & nimmt eine Referenz
    drucken("direkt");          // String-Literal ist schon &str
    println!("{text}");        // text weiterhin gültig
}

Idiomatisch: &str statt &String als Parameter — &String wird via Deref-Coercion automatisch zu &str, aber &str akzeptiert zusätzlich String-Literale ohne Konvertierung.

&mut T — mutable Referenz

Wenn die Funktion mutieren soll:

Rust &mut
fn anhaengen(s: &mut String, suffix: &str) {
    s.push_str(suffix);
}

fn main() {
    let mut text = String::from("Hallo");
    anhaengen(&mut text, ", Welt!");
    println!("{text}");        // "Hallo, Welt!"
}

Zwei Bedingungen für &mut:

  1. Die Bindung muss mut sein (let mut text).
  2. Die Referenz muss als &mut text übergeben werden.

Der Borrow Checker stellt sicher, dass keine zweite Referenz parallel existiert.

Wann welcher Modus?

SituationWahl
Kleine Copy-Typen (i32, bool, char)by-value
Read-only auf größere Daten&T
Schreiben auf größere Daten&mut T
Verbrauchen (Ownership übernehmen)by-value (non-Copy)
Optional schreiben (Builder-Pattern)mut self → Self
String-Parameter&str (akzeptiert beides)
Slice-Parameter&[T] (akzeptiert Array, Vec, Sub-Slice)

Faustregel: so viel borrowen wie möglich, so wenig moven wie nötig. Move ist gewollt, wenn die Funktion die Daten festhalten oder transformieren will. Ansonsten reicht eine Referenz.

Mehrere Rückgabewerte mit Tupel

Rust Tupel-Rückgabe
fn min_max(zahlen: &[i32]) -> (i32, i32) {
    let mut min = zahlen[0];
    let mut max = zahlen[0];
    for &n in zahlen {
        if n < min { min = n; }
        if n > max { max = n; }
    }
    (min, max)
}

fn main() {
    let (lo, hi) = min_max(&[3, 1, 4, 1, 5, 9, 2, 6]);
    assert_eq!((lo, hi), (1, 9));
}

Tupel sind perfekt für 2-3 zusammengehörige Werte. Bei mehr lohnt sich ein Struct mit benannten Feldern — sonst muss der Aufrufer raten, was .0, .1, .2 bedeuten.

Mit eigenem Struct

Rust Struct als Rückgabe
struct Stats {
    min: f64,
    max: f64,
    mittel: f64,
}

fn analysiere(werte: &[f64]) -> Stats {
    let mut min = werte[0];
    let mut max = werte[0];
    let mut summe = 0.0;
    for &v in werte {
        if v < min { min = v; }
        if v > max { max = v; }
        summe += v;
    }
    Stats { min, max, mittel: summe / werte.len() as f64 }
}

Stats ist semantisch klarer als (f64, f64, f64). Beim Aufrufen schreibst du s.min statt s.0.

Result<T, E> und Option<T> als Rückgabe

Standard-Patterns für fehlbare und optionale Rückgaben:

Rust Result und Option
fn parse_alter(s: &str) -> Result<u8, &'static str> {
    let n: u8 = s.trim().parse().map_err(|_| "kein u8")?;
    if n > 120 {
        return Err("unrealistisch");
    }
    Ok(n)
}

fn finde(slice: &[i32], ziel: i32) -> Option<usize> {
    for (i, &v) in slice.iter().enumerate() {
        if v == ziel { return Some(i); }
    }
    None
}
  • Result<T, E> — Erfolg mit Wert oder Fehler mit Information.
  • Option<T> — Wert vorhanden oder nicht (kein Fehler-Kontext).

Beide bekommen ein eigenes Kapitel im Error-Handling- und Pattern-Matching-Bereich.

Self-Returns für Builder-Pattern

Wenn eine Methode Self zurückgibt, kann sie chained werden:

Rust Builder mit Self-Return
pub struct Anfrage {
    url: String,
    timeout_ms: u32,
    retries: u32,
}

impl Anfrage {
    pub fn neu(url: impl Into<String>) -> Self {
        Anfrage { url: url.into(), timeout_ms: 5000, retries: 0 }
    }

    pub fn timeout(mut self, ms: u32) -> Self {
        self.timeout_ms = ms;
        self
    }

    pub fn retries(mut self, n: u32) -> Self {
        self.retries = n;
        self
    }
}

fn main() {
    let a = Anfrage::neu("https://example.com")
        .timeout(10_000)
        .retries(3);
    println!("{} (timeout={}, retries={})", a.url, a.timeout_ms, a.retries);
}

Drei Eigenheiten:

  • mut self als Receiver — die Methode bekommt den Builder by-value und mutiert ihn.
  • -> Self — sie gibt ihn zurück.
  • Aufruf-Kette mit .method().method() — der Builder wird durchgereicht.

Eines der idiomatischsten Patterns in Rust für konfigurierbare Konstruktoren.

impl Trait als Rückgabe

Wenn du eine Funktion schreibst, die einen Iterator oder eine Closure zurückgibt, ist impl Trait die kompakte Form:

Rust impl Iterator
fn gerade_zahlen(bis: u32) -> impl Iterator<Item = u32> {
    (0..bis).filter(|n| n % 2 == 0)
}

fn main() {
    for n in gerade_zahlen(10) {
        print!("{n} ");        // 0 2 4 6 8
    }
}

impl Iterator<Item = u32> ist ein opaker Typ: der Aufrufer weiß, dass er einen Iterator über u32 bekommt, kennt aber den konkreten Typ (Filter<Range<u32>, _>) nicht.

Vorteil: lesbar, ohne dass der Aufrufer mit komplexen generischen Typen kämpft. Einschränkung: nur ein konkreter Typ pro Funktion — kein „mal Iterator A, mal Iterator B".

Praxis: Parameter und Rückgabe im echten Code

Slice-Parameter statt Vec

Rust Idiomatischer Vec-Konsument
// Idiomatisch: &[T] akzeptiert Vec, Array, Sub-Slice
pub fn summe(werte: &[i64]) -> i64 {
    werte.iter().sum()
}

fn main() {
    let v = vec![1, 2, 3];
    let a = [10, 20, 30];
    assert_eq!(summe(&v), 6);
    assert_eq!(summe(&a), 60);
    assert_eq!(summe(&v[1..]), 5);
}

&[T] ist allgemeiner als &Vec<T>. Akzeptiert auch Arrays und Sub-Slices, was den Aufrufer flexibel macht.

Read-Only-Methode mit &self

Rust Konto-Saldo
pub struct Konto { saldo_cent: i64 }

impl Konto {
    pub fn saldo_in_euro(&self) -> f64 {
        self.saldo_cent as f64 / 100.0
    }

    pub fn ist_im_minus(&self) -> bool {
        self.saldo_cent < 0
    }
}

&self für reine Getter — kein Mutation, mehrere &self-Calls können parallel laufen (in der gleichen Funktion).

Mutable-Methode mit &mut self

Rust Konto-Mutation
# pub struct Konto { saldo_cent: i64 }
impl Konto {
    pub fn einzahlen(&mut self, cent: i64) -> Result<(), &'static str> {
        if cent <= 0 {
            return Err("Betrag muss positiv sein");
        }
        self.saldo_cent += cent;
        Ok(())
    }

    pub fn abheben(&mut self, cent: i64) -> Result<(), &'static str> {
        if cent <= 0 { return Err("Betrag muss positiv sein"); }
        if self.saldo_cent < cent { return Err("kein Guthaben"); }
        self.saldo_cent -= cent;
        Ok(())
    }
}

&mut self für Mutationen, Result<(), &str> als Rückgabe für Erfolg/Fehler ohne Daten.

Funktion mit mehreren Rückgabewerten

Rust HTTP-Response
pub fn http_response_parsen(rohdaten: &[u8]) -> Result<(u16, Vec<u8>), &'static str> {
    // Vereinfacht: nur Status-Code und Body
    let als_str = std::str::from_utf8(rohdaten).map_err(|_| "kein UTF-8")?;
    let (status_zeile, rest) = als_str.split_once("\r\n").ok_or("kein Header-Ende")?;
    let status: u16 = status_zeile
        .split_whitespace()
        .nth(1)
        .ok_or("kein Status")?
        .parse()
        .map_err(|_| "Status nicht numerisch")?;
    let body = rest.find("\r\n\r\n").map(|i| rest[i + 4..].as_bytes().to_vec())
        .unwrap_or_default();
    Ok((status, body))
}

Tupel (u16, Vec<u8>) für Status und Body — bei zwei semantisch unterschiedlichen Werten ist das idiomatisch.

Verbrauchende Methode mit self

Rust Konsumierender Build
pub struct StringBuilder { teile: Vec<String> }

impl StringBuilder {
    pub fn neu() -> Self {
        StringBuilder { teile: Vec::new() }
    }

    pub fn anhaengen(mut self, s: &str) -> Self {
        self.teile.push(s.to_string());
        self
    }

    pub fn bauen(self) -> String {
        self.teile.join("")
    }
}

fn main() {
    let s = StringBuilder::neu()
        .anhaengen("Hallo")
        .anhaengen(", ")
        .anhaengen("Welt!")
        .bauen();
    assert_eq!(s, "Hallo, Welt!");
}

bauen(self) verbraucht den Builder — die Instanz ist danach nicht mehr nutzbar, weil ihre Daten in den String umgezogen sind. Klar im Typ-System ausgedrückt.

impl Iterator als Rückgabe

Rust Iterator-Factory
pub fn primzahlen_bis(n: u32) -> impl Iterator<Item = u32> {
    (2..n).filter(|&k| (2..k).all(|d| k % d != 0))
}

fn main() {
    let p: Vec<u32> = primzahlen_bis(20).collect();
    assert_eq!(p, vec![2, 3, 5, 7, 11, 13, 17, 19]);
}

Der Aufrufer arbeitet mit einem Iterator — kann lazy filtern, mappen oder collecten. Der konkrete Typ ist intern verborgen.

Trait-Bound auf generischem Parameter

Rust Generischer Logger
use std::fmt::Display;

pub fn log_wert<T: Display>(name: &str, wert: T) {
    eprintln!("[INFO] {name} = {wert}");
}

fn main() {
    log_wert("count", 42);
    log_wert("name", "Anna");
    log_wert("aktiv", true);
}

T: Display bedeutet: jeder Typ, der per {} formatiert werden kann, wird akzeptiert. Sehr flexibel ohne Type-Explosion.

Closure als Parameter

Rust Higher-Order
pub fn retry<T, E, F>(versuche: u32, mut f: F) -> Result<T, E>
where
    F: FnMut() -> Result<T, E>,
{
    let mut letzter_fehler = None;
    for _ in 0..versuche {
        match f() {
            Ok(v) => return Ok(v),
            Err(e) => letzter_fehler = Some(e),
        }
    }
    Err(letzter_fehler.unwrap())
}

FnMut() -> Result<T, E> ist eine Closure, die mutieren darf und ein Result liefert. Wird oft genau so in HTTP-Retry-Libraries verwendet.

FAQ

Wann &str, wann String als Parameter?

Fast immer &str. String als Parameter signalisiert „die Funktion will Ownership übernehmen" — sinnvoll, wenn der String festgehalten oder transformiert wird. Sonst ist &str flexibler (akzeptiert beides via Deref).

Soll ich Werte by-value oder per Referenz übergeben?

Copy-Typen (i32, bool, char, kleine Tupel) → by-value (kostenlos kopiert). Größere Strukturen oder non-Copy-Typen → meist &T (Borrow). Nur wenn die Funktion die Daten übernimmt → by-value (Move).

Was passiert mit Move-Parametern bei Funktions-Ende?

Sie werden gedroppt — wie jede andere Bindung in einem Scope. Ein String-Parameter wird beim Funktions-Return freigegeben (Drop für String gibt das Heap-Allokat frei).

Kann ich mehrere mutable Referenzen übergeben?

Auf unterschiedliche Daten ja. Auf dieselben Daten nein — der Borrow Checker erlaubt nur eine &mut-Referenz auf einen Wert zur gleichen Zeit. Bei Methoden mit &mut self plus weiterem &mut-Parameter auf dasselbe Objekt muss man umstrukturieren.

Warum gibt es keine Default-Werte für Parameter?

Sprach-Designentscheidung: Default-Parameter machen Funktions-Signaturen unklar (welcher Wert wird genommen?). Rust setzt auf explizite Builder-Pattern, Options-Structs oder mehrere benannte Konstruktor-Funktionen.

impl Trait in Rückgabe vs. Box?

impl Trait ist statisch — der konkrete Typ ist zur Compile-Zeit bekannt, Performance ist optimal, aber nur ein konkreter Typ pro Funktion möglich. Box<dyn Trait> ist dynamisch — Heap-allokiert, virtueller Dispatch, aber verschiedene Implementierungen je nach Code-Pfad zurückgebbar. Faustregel: impl Trait wenn möglich.

Lifetime-Annotationen oft optional.

Rust hat Elision-Regeln: fn foo(s: &str) -> &str ist äquivalent zu fn foo<'a>(s: &'a str) -> &'a str. Der Compiler ergänzt das automatisch. Explizite Lifetimes brauchst du erst bei mehreren Eingabe-Referenzen mit unterschiedlichen Lebenszeiten — siehe Lifetimes-Kapitel.

impl Into als Parameter.

Sehr flexibles Pattern: fn neu(name: impl Into<String>) akzeptiert &str, String, Cow<str> und alles weitere mit Into<String>-Impl. Klassisch in Builder-Konstruktoren.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Funktionen

Zur Übersicht