Funktionsaufrufe sind in Rust die häufigste Stelle, an der Ownership-Entscheidungen sichtbar werden. Eine Funktion kann ihre Parameter übernehmen (by-value, Move), lesend borgen (&T) oder mutierend borgen (&mut T). Jede Wahl hat unterschiedliche Auswirkungen auf den Aufrufer, auf Lifetime-Beschränkungen und auf API-Klarheit. Dieser Artikel zeigt die drei Modi systematisch, illustriert das Round-Trip-Pattern (Funktion gibt einen gemovedten Wert zurück), und gibt klare Faustregeln für gutes API-Design.

Die drei Parameter-Modi

ModusSyntaxWas passiert
By-valuefn foo(x: T)Move (oder Copy bei Copy-Typen) — Aufrufer verliert Ownership
Shared Borrowfn foo(x: &T)Borrow — Aufrufer behält, Funktion liest
Mutable Borrowfn foo(x: &mut T)Borrow — Aufrufer behält, Funktion schreibt
Rust Alle drei Modi
fn nehmen(s: String) {           // by-value: Move
    println!("verbraucht: {s}");
}   // s wird hier gedroppt

fn lesen(s: &String) {            // &T: lesend borgen
    println!("gelesen: {s}");
}

fn modifizieren(s: &mut String) { // &mut T: schreibend borgen
    s.push_str(", Welt");
}

fn main() {
    let mut text = String::from("Hi");

    lesen(&text);                 // & — text bleibt nutzbar
    modifizieren(&mut text);      // &mut — text bleibt nutzbar
    println!("nach mod: {text}"); // "Hi, Welt"

    nehmen(text);                 // Move — text danach weg
    // println!("{text}");        // Fehler — gemoved.
}

By-value: wenn die Funktion „übernimmt"

Du nimmst einen Parameter by-value, wenn die Funktion den Wert behalten oder verbrauchen will:

Rust Verbrauchend
struct Server { konfig: String }

impl Server {
    fn aus_config(c: String) -> Self {       // c wird behalten
        Server { konfig: c }
    }
}

fn main() {
    let c = String::from("config.toml");
    let server = Server::aus_config(c);
    // c hier nicht mehr nutzbar — gemoved in den Server.
}

Die Funktion aus_config übernimmt c und packt es in den Struct. Der Aufrufer verliert Zugriff auf c — was semantisch korrekt ist, denn der Server „besitzt" jetzt die Config.

Typische by-value-Patterns

  • Konstruktoren, die Werte in einen Struct übernehmen.
  • Builder-Methoden mit self-Receiver (Method-Chaining).
  • into-artige Konvertierungsfunktionen.
  • Funktionen mit konsumierender Semantik: Channel-Send, Thread-Spawn.

&T: lesend borgen — die häufigste Wahl

Rust Shared Borrow
fn zaehle_zeichen(s: &String) -> usize {
    s.chars().count()
}

fn ist_palindrom(s: &str) -> bool {
    let chars: Vec<char> = s.chars().collect();
    chars == chars.iter().rev().copied().collect::<Vec<_>>()
}

fn main() {
    let text = String::from("anna");
    let anzahl = zaehle_zeichen(&text);
    let palin = ist_palindrom(&text);
    println!("{text} hat {anzahl} Zeichen, Palindrom: {palin}");
}

&T ist die richtige Wahl, wenn die Funktion nur liest. Vorteile:

  • Aufrufer behält Ownership — kann den Wert nach dem Call weiter nutzen.
  • Mehrere &T parallel erlaubt — kein Lock-artiger Konflikt.
  • Cheap: nur ein Pointer wird übergeben.

&T vs. &str-Empfehlung

Rust Idiomatisch
// Weniger flexibel:
fn weniger_gut(s: &String) -> usize { s.len() }

// Flexibler:
fn besser(s: &str) -> usize { s.len() }

fn main() {
    let owned = String::from("Hi");
    let literal = "Hi";

    besser(&owned);      // ok
    besser(literal);     // ok — String-Literal direkt
    // weniger_gut(literal);   // Fehler — &String erwartet, &str gegeben
}

Bei String-Parametern: &str statt &String. Bei Slice-Parametern: &[T] statt &Vec<T>. Der allgemeinere Typ ist immer flexibler.

&mut T: schreibend borgen

Rust Mutable Borrow
fn anhaengen_zeilennummer(text: &mut String, nummer: u32) {
    text.push_str(&format!(" [Zeile {nummer}]"));
}

fn main() {
    let mut log = String::from("Etwas passiert");
    anhaengen_zeilennummer(&mut log, 42);
    println!("{log}");        // "Etwas passiert [Zeile 42]"
}

Voraussetzungen für &mut T:

  • Die Bindung im Aufrufer muss mut sein (let mut log = ...).
  • Beim Call: &mut log (mit dem &mut).

Der Borrow Checker stellt sicher, dass keine zweite Referenz (egal ob & oder &mut) parallel existiert.

Round-Trip: Move + Rückgabe

Manchmal will eine Funktion Ownership übernehmen, mutieren, und den Wert dann zurückgeben. Das war vor &mut der einzige Weg — heute meist überholt, aber gelegentlich noch sinnvoll:

Rust Round-Trip
fn anhaengen(mut s: String, suffix: &str) -> String {
    s.push_str(suffix);
    s          // gibt den modifizierten String zurück
}

fn main() {
    let mut t = String::from("Hi");
    t = anhaengen(t, ", Welt");      // Move rein, Move raus
    println!("{t}");        // "Hi, Welt"
}

Funktioniert, ist aber verbose. Idiomatischer:

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

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

Round-Trip ist sinnvoll bei:

  • Builder-Methoden mit self-Receiver (Method-Chaining).
  • Pure Transformationen, wo der „neue" Wert konzeptuell etwas anderes ist als der alte (z. B. let upper = uppercase(s)).
  • Method-Chaining-freundliche APIs.

Welcher Modus, wann?

Faustregeln für API-Design:

SituationModus
Funktion liest nur&T (oder &str/&[T] bei Strings/Slices)
Funktion mutiert, gibt nichts zurück&mut T
Funktion übernimmt und behält (Struct-Init)by-value
Builder mit Method-Chainself als by-value, -> Self
Copy-Typ (i32, bool, ...)by-value (gleich teuer wie &T)
Funktion erstellt neuen Wert ohne Mutation&T rein, owned Wert raus

Beispiel-Galerie

Rust Typische Signaturen
// Lesen: &T
fn ist_email(s: &str) -> bool { s.contains('@') }
fn summe(zahlen: &[i32]) -> i32 { zahlen.iter().sum() }

// Mutieren: &mut T
fn sortieren(daten: &mut Vec<i32>) { daten.sort(); }
fn aufrufen<F: FnMut()>(mut f: F) { f(); }

// By-value bei Übernahme
fn channel_send(tx: &std::sync::mpsc::Sender<String>, payload: String) {
    tx.send(payload).unwrap();    // payload wird verbraucht
}

// Builder mit Method-Chain
struct B(Vec<i32>);
impl B {
    fn neu() -> Self { B(vec![]) }
    fn mit(mut self, n: i32) -> Self { self.0.push(n); self }
    fn bauen(self) -> Vec<i32> { self.0 }
}

// Pure Transformation
fn upper(s: &str) -> String { s.to_uppercase() }

Häufige Stolperfallen

Borrowing während Move-Versuch

Rust Borrow + Move-Konflikt
fn drucke(s: &String) { println!("{s}"); }
fn nehmen(s: String) { drop(s); }

fn main() {
    let s = String::from("Hi");
    let r = &s;
    // nehmen(s);                  // Fehler — kann s nicht moven, solange r lebt
    drucke(r);
    // Nach drucke(r) ist r nicht mehr in Verwendung — jetzt geht es:
    nehmen(s);                     // ok
}

Ein aktiver Borrow &s verhindert Move von s. Der Borrow Checker erkennt aber, dass r nach der letzten Verwendung „tot" ist — daher klappt der spätere nehmen(s).

Mutable Borrow vs. shared Borrow

Rust Borrow-Konflikt
fn main() {
    let mut v = vec![1, 2, 3];
    let r1 = &v;
    // v.push(4);                  // Fehler — &v lebt noch
    let _ = r1;
    v.push(4);                     // ok — r1 nicht mehr in Verwendung
}

Die Borrow-Regeln: entweder mehrere &T ODER genau ein &mut T, nie beides parallel. Mehr im References-Kapitel.

Verschachtelte Calls mit &mut

Rust Doppeltes &mut
fn main() {
    let mut v = vec![1, 2, 3];
    // v.push(v.len() as i32);     // Fehler — v wird gleichzeitig als &v und &mut v gebraucht
    let len = v.len() as i32;
    v.push(len);                   // ok
}

v.push(v.len()) braucht gleichzeitig einen &mut v (für push) und einen &v (für len) — Verstoß gegen Borrow-Regel. Lösung: len vorher extrahieren.

Praxis: Ownership-Patterns im echten Code

HTTP-Handler: by-ref nehmen, by-value zurückgeben

Rust HTTP-Handler
struct Request<'a> { path: &'a str, body: Vec<u8> }
struct Response { status: u16, body: Vec<u8> }

fn handle_get(req: &Request) -> Response {
    // req nur lesen
    let body = format!("Du hast {} angefragt", req.path).into_bytes();
    Response { status: 200, body }
}

fn main() {
    let req = Request { path: "/users", body: vec![] };
    let resp = handle_get(&req);
    println!("{}", resp.status);
    // req noch nutzbar
}

Klassisches Pattern: Request per Borrow lesen, Response by-value zurückgeben. Der Handler verbraucht nichts vom Aufrufer-Kontext.

Sammlung verarbeiten mit &mut

Rust In-Place-Update
fn normalisiere(zahlen: &mut Vec<f64>) {
    let max = zahlen.iter().cloned().fold(0.0f64, f64::max);
    if max > 0.0 {
        for n in zahlen.iter_mut() {
            *n /= max;
        }
    }
}

fn main() {
    let mut v = vec![1.0, 2.0, 4.0];
    normalisiere(&mut v);
    assert_eq!(v, vec![0.25, 0.5, 1.0]);
}

&mut Vec<f64> für In-Place-Mutation. Klassische API-Form für „verändere die Sammlung an Ort und Stelle".

Channel-Send als Verbraucher

Rust Channel-Send
use std::sync::mpsc::Sender;

fn versende_jobs(tx: &Sender<String>, jobs: Vec<String>) {
    // jobs wird hier konsumiert — jeder Job wird in den Channel gemoved
    for job in jobs {
        tx.send(job).unwrap();
    }
}

Vec<String> by-value (die Sammlung wird verbraucht). Sender per &, weil der Sender als Handle weiter nutzbar bleiben soll.

Builder mit self-Konsumption

Rust Builder
struct Anfrage {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
}

impl Anfrage {
    fn neu(url: impl Into<String>) -> Self {
        Anfrage {
            url: url.into(),
            method: "GET".into(),
            headers: Vec::new(),
            body: Vec::new(),
        }
    }
    fn method(mut self, m: &str) -> Self { self.method = m.to_string(); self }
    fn header(mut self, k: &str, v: &str) -> Self {
        self.headers.push((k.to_string(), v.to_string())); self
    }
    fn body(mut self, b: Vec<u8>) -> Self { self.body = b; self }
}

fn main() {
    let a = Anfrage::neu("https://example.com/api")
        .method("POST")
        .header("Content-Type", "application/json")
        .body(b"{}".to_vec());
    println!("{}: {}", a.method, a.url);
}

Jede Builder-Methode konsumiert self, mutiert intern, gibt self zurück. Ermöglicht Method-Chaining ohne &mut-Bindung im Aufrufer.

Pure Transformation: Borrow rein, owned raus

Rust Transform
fn doppelt(zahlen: &[i32]) -> Vec<i32> {
    zahlen.iter().map(|&n| n * 2).collect()
}

fn main() {
    let original = vec![1, 2, 3];
    let neu = doppelt(&original);
    assert_eq!(original, vec![1, 2, 3]);       // unverändert
    assert_eq!(neu, vec![2, 4, 6]);
}

Die Funktion liest das Slice, allokiert einen neuen Vec, gibt ihn zurück. Aufrufer behält das Original, bekommt einen neuen, unabhängigen Wert.

Thread mit Move-Closure

Rust Thread-Owner
use std::thread;

fn main() {
    let daten: Vec<u64> = (1..=1_000_000).collect();
    let handle = thread::spawn(move || {
        let summe: u64 = daten.iter().sum();
        summe
    });
    // daten ist hier weg — Thread besitzt es
    let summe = handle.join().unwrap();
    println!("{summe}");
}

move in der Closure überträgt Ownership in den Thread. Damit ist keine Sync-Kommunikation nötig — der Thread hat seine eigenen Daten.

Read-Only-API mit AsRef

Rust AsRef
fn dateigroesse_byte<P: AsRef<std::path::Path>>(pfad: P) -> std::io::Result<u64> {
    let metadata = std::fs::metadata(pfad)?;
    Ok(metadata.len())
}

fn main() -> std::io::Result<()> {
    let s = String::from("/tmp/foo.txt");
    let _ = dateigroesse_byte(&s);          // &String
    let _ = dateigroesse_byte("/tmp/bar.txt");      // &str
    let _ = dateigroesse_byte(std::path::PathBuf::from("/tmp/baz.txt"));   // PathBuf
    Ok(())
}

AsRef<Path> als Trait-Bound nimmt alles, was sich zu einem Pfad referenzieren lässt. Sehr flexibel — kostenlos, denn der Borrow ist nur ein Pointer.

State-Migration mit mem::replace

Rust State-Wechsel
use std::mem;

#[derive(Debug)]
enum Verbindung {
    Inaktiv,
    Verbunden(String),
}

fn schliessen(v: &mut Verbindung) -> Verbindung {
    mem::replace(v, Verbindung::Inaktiv)        // alten Wert raus, Inaktiv rein
}

fn main() {
    let mut v = Verbindung::Verbunden("peer-42".into());
    let alt = schliessen(&mut v);
    println!("Geschlossen: {alt:?}");           // Verbunden("peer-42")
    println!("Aktuell: {v:?}");                 // Inaktiv
}

mem::replace ermöglicht „nimm den Wert hinter einer &mut-Referenz heraus und ersetze ihn durch einen anderen". Klassisch für State-Machine-Transitionen.

Häufige Stolperfallen

fn foo(s: &String) ist fast immer falsch.

Mit &str ist die Funktion flexibler (akzeptiert auch String-Literale ohne Konvertierung). Clippy warnt mit clippy::ptr_arg. Außer in sehr seltenen Fällen sollte ein String-Parameter &str heißen.

self per Wert verbraucht — wer das Objekt nochmal braucht, klont.

Wenn eine Methode self (ohne &) als Receiver hat, ist das Objekt nach dem Call weg. Wer den Wert weiter braucht, muss vorher .clone() rufen — oder die API anpassen.

&mut self bei nicht-mut-Bindung — Compile-Fehler.

let v = vec![]; v.push(1); schlägt fehl, weil push ein &mut self braucht — die Bindung v ist aber nicht mut. Lösung: let mut v = vec![];. rustc-Suggestion sagt das direkt.

Zwei &mut auf das gleiche Objekt zur gleichen Zeit — verboten.

Der Borrow Checker erlaubt nur EINEN aktiven &mut-Borrow. Wer parallel zwei Funktionen mit &mut-Parameter aufrufen will, muss die Calls sequenziell machen oder die Mutex-/Interior-Mutability-Patterns benutzen.

Methoden-Receiver beeinflussen Ownership.

&self — Read-Only, Aufrufer behält. &mut self — Schreiben, Aufrufer behält, braucht mut-Bindung. self — Verbrauchend, Objekt danach weg. Die Wahl prägt die ganze API.

Cow als Parameter für „accept beides“.

Wenn eine Funktion sowohl &str als auch owned String akzeptieren soll, ist Cow<'_, str> als Parameter elegant — oder generisch mit impl Into<String>. Beides idiomatisch.

Closure-Captures folgen Ownership-Regeln.

Eine Closure ohne move borrowt — ihre Captures müssen die Closure überleben. Mit move werden Captures gemoved — danach hat der Aufrufer keinen Zugriff mehr. Wichtig bei Thread-Spawning.

Zurück-Move-Round-Trip wird durch Closures überflüssig.

Statt t = anhaengen(t, "...") lieber anhaengen(&mut t, "..."). Round-Trip-Moves sind syntaktisch schwerer, semantisch identisch zu &mut-Patterns. Außer für Method-Chaining-Builder: dort ist Round-Trip mit self idiomatisch.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Ownership

Zur Übersicht