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.
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.
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.
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:
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:
- Die Bindung muss
mutsein (let mut text). - Die Referenz muss als
&mut textübergeben werden.
Der Borrow Checker stellt sicher, dass keine zweite Referenz parallel existiert.
Wann welcher Modus?
| Situation | Wahl |
|---|---|
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
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
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:
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:
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 selfals 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:
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
// 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
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
# 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
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
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
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
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
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
- The Rust Book – Functions
- The Rust Book – References and Borrowing
- Rust Reference – Function Items
- Rust API Guidelines – Future-Proofing
- Clippy –
ptr_arg-Lint