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
Eine Rust-Funktions-Signatur ist immer auch eine Ownership-Aussage: durch die Wahl des Parameter-Typs sagt sie, wie die Funktion mit dem Wert umgeht. Diese drei Modi sind die Bausteine, aus denen jede Funktions-API in Rust aufgebaut ist — und die richtige Wahl ist eine der wichtigsten Design-Entscheidungen, weil sie sich direkt auf die Aufrufer-Seite auswirkt.
| Modus | Syntax | Was passiert |
|---|---|---|
| By-value | fn foo(x: T) | Move (oder Copy bei Copy-Typen) — Aufrufer verliert Ownership |
| Shared Borrow | fn foo(x: &T) | Borrow — Aufrufer behält, Funktion liest |
| Mutable Borrow | fn foo(x: &mut T) | Borrow — Aufrufer behält, Funktion schreibt |
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:
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
By-value-Parameter sind weniger häufig als Borrows, haben aber klare Anwendungsfälle:
Konstruktoren, die Werte in einen Struct übernehmen — etwa Server::aus_config(c: String) oben. Der Konstruktor will den Wert als sein eigenes Feld halten, also macht Move semantischen Sinn.
Builder-Methoden mit self-Receiver (fn mit_header(mut self, ...) -> Self). Jeder Builder-Schritt konsumiert das aktuelle Builder-Objekt und gibt eine modifizierte Version zurück, was Method-Chaining ergibt.
into-artige Konvertierungsfunktionen wie s.to_uppercase() oder vec.into_iter() — sie nehmen den Wert by-value, weil das Ergebnis eine neue, separate Repräsentation ist und das Original nicht mehr gebraucht wird.
Funktionen mit konsumierender Semantik: Sender::send(value), thread::spawn(closure). Hier ist Move zwingend, weil der Wert die aktuelle Scope-Grenze überschreitet — er muss endgültig übergeben werden.
&T: lesend borgen — die häufigste Wahl
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 — und das ist in der Praxis die mit Abstand häufigste Situation. Drei Eigenschaften machen Shared Borrows zum Default-Werkzeug:
Aufrufer behält Ownership. Nach dem Call ist der Wert noch da, kann weiter verwendet werden. Das ist intuitiv das, was man fast immer will — eine Funktion soll dir den Wert nicht „wegnehmen", nur lesen.
Mehrere &T parallel erlaubt. Du kannst denselben Wert an beliebig viele Funktionen gleichzeitig per Shared Borrow übergeben, oder mehrere Borrows im selben Scope halten. Anders als bei &mut T gibt es keine Exklusivitäts-Anforderung — Shared Reads sind unbeschränkt.
Cheap. Bei der Übergabe wird nur ein Pointer kopiert (8 Bytes auf 64-bit), unabhängig von der Größe des Originals. Bei einem &Vec<i32> mit 10 000 Elementen wird kein Inhalt kopiert — nur der Header-Pointer.
&T vs. &str-Empfehlung
// 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 ist &str statt &String praktisch immer die bessere Wahl, und ebenso &[T] statt &Vec<T> bei Slice-Parametern. Der Grund: &String akzeptiert nur Werte, die als String deklariert sind, während &str zusätzlich auch String-Literale ("foo"), Sub-Slices (&s[1..3]) und Werte aus anderen String-Quellen direkt annimmt. Die Funktion verliert nichts an Funktionalität — sie gewinnt Flexibilität.
Im Hintergrund passiert das über Deref-Coercion: String implementiert Deref<Target = str>, sodass eine &String automatisch zu &str wird, wenn der Compiler das erwartet. Ein Vec<T> zu &[T] funktioniert genauso. Clippy warnt mit clippy::ptr_arg, wenn du &String oder &Vec<T> in einem Parameter siehst — und in den allermeisten Fällen ist die Warnung berechtigt.
&mut T: schreibend borgen
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]"
}&mut T ist mehr als nur die mutable Variante von &T — es bringt eine wichtige zusätzliche Garantie mit sich: Exklusivität. Solange der &mut-Borrow aktiv ist, darf es keine andere Referenz auf den Wert geben (weder & noch &mut). Diese Aliasing-Garantie ist der Grund, warum Mutation in Rust safe ist — der Compiler weiß zu jedem Zeitpunkt genau, wer den Wert verändern darf.
Voraussetzungen, die der Aufrufer erfüllen muss:
Die Bindung muss mut sein (let mut log = ...). Eine immutable Bindung kann keinen mutable Borrow herausgeben — selbst wenn der innere Wert mutabel verwendet werden könnte.
Beim Call muss explizit &mut stehen (anhaengen_zeilennummer(&mut log, 42)). Diese explizite Annotation hat einen wichtigen Lesbarkeitseffekt: am Call-Site sieht ein Reviewer sofort, dass diese Funktion den Wert verändern kann. Es gibt keinen versteckten Side-Effect — wenn keine &mut da steht, wird der Wert nicht modifiziert.
Der Borrow Checker stellt zur Compile-Zeit sicher, dass die Aliasing-Regel eingehalten wird. Versuche, parallel einen weiteren Borrow zu nehmen, scheitern mit dem berüchtigten cannot borrow ... as mutable more than once-Fehler. Das ist nicht eine Schikane — es ist die Grundlage dafür, dass Rust ohne Garbage Collector und Locks safe sein kann.
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:
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:
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-Patterns sind nicht generell schlecht — sie sind in spezifischen Situationen sogar idiomatisch. Drei sinnvolle Anwendungsfälle:
Builder-Methoden mit self-Receiver (fn mit_header(mut self, ...) -> Self). Hier ermöglicht das Round-Trip-Pattern Method-Chaining wie builder.foo().bar().baz() — jede Methode konsumiert und gibt wieder zurück, sodass die Aufrufe sich kettenartig verbinden lassen.
Pure Transformationen, bei denen der „neue" Wert konzeptuell etwas anderes ist als der alte. let upper = s.to_uppercase() ist eine Transformation, kein In-Place-Update — semantisch entsteht ein neuer Wert. Hier wäre &mut self semantisch falsch, weil die Operation den ursprünglichen Wert logisch nicht verändert, sondern eine Ableitung produziert.
Method-Chaining-freundliche APIs, bei denen der Aufrufer Werte oft direkt weiterverarbeitet. Mit Round-Trip braucht er keine mutable Bindung, kein let mut x = ...; mutate(&mut x); use(x) — der modifizierte Wert kommt direkt aus dem Funktionsaufruf zurück.
Welcher Modus, wann?
Faustregeln für API-Design:
| Situation | Modus |
|---|---|
| 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-Chain | self 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
// 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
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
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
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
Die folgenden Beispiele zeigen jeweils einen der drei Modi in einem realistischen Kontext. Beim Lesen lohnt es sich, jeweils zu fragen: warum genau dieser Modus, und nicht einer der beiden anderen?
HTTP-Handler: by-ref nehmen, by-value zurückgeben
struct Request { path: String, 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: String::from("/users"), body: vec![] };
let resp = handle_get(&req);
println!("{}", resp.status);
// req noch nutzbar
}Bei HTTP-Handlern ist die Trennung sauber: der Request kommt per Borrow, die Response per Value zurück. Der Handler greift lesend auf den Request zu (er ändert ihn nicht) und produziert eine neue Response, die er als sein eigenes Werk zurückgibt. Die meisten Web-Frameworks in Rust folgen diesem Pattern — axum, actix-web, warp haben alle vergleichbare Signaturen.
In produktivem Web-Code würde path häufig nicht als String gespeichert, sondern als geliehene Slice in den Request-Buffer — das spart die Kopie. Diese Variante braucht Lifetime-Annotationen und wird im Lifetimes-Kapitel gezeigt.
Sammlung verarbeiten mit &mut
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]);
}In-Place-Mutation ist effizient: die Sammlung wird nicht kopiert, der Aufrufer behält Ownership, und die Veränderung passiert direkt im ursprünglichen Vec. Bei einer großen Zahlenliste mit Millionen Elementen ist das ein wichtiger Performance-Faktor — eine Variante, die einen neuen Vec produziert, müsste den gesamten Speicher kopieren.
Eine semantische Faustregel: wenn die API-Aktion das Verb „normalisieren", „sortieren", „bereinigen" oder „aktualisieren" suggeriert, ist &mut T als Parameter natürlich. Wenn sie eher „kopieren", „transformieren" oder „berechnen" heißt, ist &T plus Rückgabewert besser.
Channel-Send als Verbraucher
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();
}
}Eine schöne Demonstration der differenzierten Modus-Wahl in einer einzigen Signatur. Der Sender kommt per &Sender — er ist ein langlebiges Handle, das nach dem Funktionsaufruf weiter benutzbar bleiben soll (vielleicht ruft der Aufrufer versende_jobs mehrfach hintereinander). Der Vec<String> mit den Jobs kommt by-value — die Funktion verbraucht ihn vollständig, jeder einzelne Job wird in den Channel gemoved.
Diese Mischung — Handle per Borrow, Daten per Move — ist ein häufiges Pattern bei API-Endpunkten, die Daten in eine Pipeline geben. Der Pipeline-Eingang (Handle, Connection, Sink) wird wiederverwendet, die einzelnen Datenpakete werden verbraucht.
Builder mit self-Konsumption
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);
}Das ist der klassische Builder-Style mit self-Konsumption. Jede Methode (method, header, body) übernimmt das aktuelle Builder-Objekt, modifiziert ein Feld und gibt das geänderte Objekt zurück. Der Aufrufer braucht keine let mut-Bindung — die Method-Chain liest sich linear von oben nach unten.
Der Trick ist mut self als Receiver. Das mut erlaubt die interne Mutation, das self (ohne &) macht es zu einem Move-Receiver. Damit sind zwei Builder-Aufrufe auf demselben Wert nicht möglich (a.method("POST"); a.header(...) schlägt fehl, weil a nach dem ersten Aufruf gemoved ist) — aber das ist auch nicht gewollt: das Builder-Pattern ist explizit für Method-Chaining gedacht.
Eine alternative Variante mit &mut self würde nicht-Chaining-Aufrufe erlauben, aber jede Methode bräuchte separate Zeilen mit let mut-Bindung. Die Wahl hängt vom API-Stil ab; bei externen Library-APIs ist mut self häufiger, bei internen Helpers das &mut self-Pattern.
Pure Transformation: Borrow rein, owned raus
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 Pure-Transformation-Variante: &[i32] als Input (lesender Borrow), Vec<i32> als Output (neu allozierter Wert). Der Aufrufer behält das Original — original ist nach dem Aufruf unverändert — und bekommt zusätzlich einen neuen, unabhängigen Vec mit dem Ergebnis.
Diese Form ist semantisch sehr klar: die Funktion hat keine Side-Effects, der Input bleibt unangetastet, das Ergebnis ist ein neuer Wert. In funktionalen Stilrichtungen heißt das „immutable" oder „pure" — und Rust ist hier sehr gut darin, weil die Borrow-Semantik schon im Typ-System verankert ist. Vergleich zum mutable Update (&mut [i32]): du würdest dort den Input verändern, was zwar effizienter wäre, aber für viele Aufrufer überraschend.
Thread mit Move-Closure
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}");
}Das Thread-Pattern zeigt, warum Move-Semantik bei Concurrency so wichtig ist. Die daten werden in die Closure und damit in den Thread gemoved — der Main-Thread hat danach keinen Zugriff mehr. Genau das ist die Garantie, die du brauchst: zwei Threads können nicht gleichzeitig auf denselben mutablen Wert zugreifen, weil nur einer ihn besitzt.
thread::spawn braucht eine FnOnce + Send + 'static-Closure. 'static heißt: die Closure darf keine Borrows haben, deren Lifetime an einen Aufrufer-Scope gebunden ist — was bei move-Capture automatisch erfüllt ist, weil die Closure die Werte selbst besitzt. Ohne move hätte die Closure nur einen Borrow auf daten, der nicht 'static wäre, und der Compile würde scheitern.
Read-Only-API mit 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> ist die Trait-basierte Verallgemeinerung des Borrow-Patterns. Statt einen konkreten Borrow-Typ wie &str oder &Path zu verlangen, akzeptierst du alles, was sich als dieser Typ referenzieren lässt — String, PathBuf, &str, &Path, eigene Path-Wrapper. Im Funktions-Body rufst du pfad.as_ref() auf und bekommst eine &Path, die du wie üblich verwendest.
Diese Flexibilität ist kostenlos: zur Compile-Zeit wird AsRef monomorphisiert, sodass für jeden konkreten Typ eine separate Funktion erzeugt wird, jeweils ohne zusätzlichen Overhead. Du bekommst die Bequemlichkeit „akzeptiere alles, was wie ein Pfad aussieht" ohne Performance-Kosten. Die Stdlib nutzt das Pattern überall — std::fs::File::open, std::fs::read_to_string und viele andere haben genau diese Signatur.
State-Migration mit mem::replace
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 löst ein Problem, das mit direktem Move nicht ginge: du hast nur eine &mut-Referenz auf eine State-Variable und willst den aktuellen Wert herausholen, gleichzeitig aber den Slot mit einem anderen Wert füllen, damit der State-Container intakt bleibt. Die Operation ist atomar — sie ersetzt den alten Wert durch den neuen und gibt den alten zurück, in einem Schritt.
Bei State-Machines mit Enums ist das ein wiederkehrendes Pattern. Du willst von State A nach State B wechseln, aber der Aufrufer hat nur &mut self auf den Container. Mit mem::replace(self, NeuerState) holst du den alten State heraus, kannst seine Felder destrukturieren und in den neuen einarbeiten, und der Container hat schon den neuen State. Ohne mem::replace wäre das schwierig — du müsstest entweder klonen (teuer) oder mit unsafe-Code arbeiten.
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
- The Rust Book – References and Borrowing
- The Rust Book – Ownership and Functions
- Rust API Guidelines – Naming
- Clippy – ptr_arg
- std::mem::replace