Wenn du eine Referenz hast, willst du irgendwann auf den Wert dahinter zugreifen. Rust bietet dafür zwei Wege: explizit mit dem *-Operator, und implizit durch Auto-Deref. Auto-Deref ist eines der mächtigsten Werkzeuge der Sprache — es macht &String und &str an Funktions-Schnittstellen austauschbar, lässt Method-Calls über Smart-Pointer wie Box<T> oder Rc<T> ohne Sonderbehandlung funktionieren, und sorgt dafür, dass du r.len() schreiben kannst statt (*r).len(). Dieser Artikel zeigt, wie Dereferenzierung im Detail funktioniert, was der Deref-Trait macht, und welche Grenzen die Coercion hat.
Der *-Operator
*r ist der Dereference-Operator — er greift auf den Wert hinter einer Referenz zu:
fn main() {
let x = 5;
let r: &i32 = &x;
assert_eq!(*r, 5); // *r ergibt den i32-Wert hinter r
assert_eq!(r, &5); // r ist die Referenz selbst
}Bei Mutation eines Werts hinter &mut T braucht man * explizit:
fn main() {
let mut x = 5;
let r: &mut i32 = &mut x;
*r = 10; // Wert hinter r auf 10 setzen
*r += 1; // Wert hinter r um 1 erhöhen
assert_eq!(x, 11);
}*r = 10 schreibt in den Wert (x). Ohne * würde r = ... versuchen, die Referenz selbst neu zu binden — was Compile-Fehler ist.
Auto-Deref bei Method-Calls
Methoden-Aufrufe machen Dereferenzierung automatisch. Du schreibst nie (*r).methode(), immer r.methode():
fn main() {
let s = String::from("Hallo");
let r: &String = &s;
assert_eq!(r.len(), 5); // r.len() = (*r).len() = String::len(r)
assert_eq!(s.len(), 5); // direkt — der Compiler macht es identisch
}Der Compiler probiert beim Method-Lookup automatisch alle Dereferenzierungs-Stufen aus: r.len() → (*r).len() → schließlich findet er String::len.
Auch verschachtelt
fn main() {
let s = String::from("Hi");
let r: &&&String = &&&s; // dreifache Referenz
assert_eq!(r.len(), 2); // Compiler dereferenziert 3-mal automatisch
}Auch durch mehrere Indirektions-Ebenen findet der Compiler die Methode.
Der Deref-Trait
* ist normalerweise nur für „echte" Referenzen (&T, &mut T) definiert. Damit aber auch Smart-Pointer wie Box<T>, Rc<T>, String dereferenzierbar sind, gibt es den Deref-Trait:
// Aus der Stdlib:
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}Ein Typ, der Deref implementiert, sagt: „wenn du * auf mich anwendest, bekommst du &Target".
String implementiert Deref<Target = str>
fn main() {
let s: String = String::from("Hi");
let r: &str = &*s; // *s gibt str, &*s gibt &str
// Oder mit Auto-Deref:
let r2: &str = &s; // implizite Deref-Coercion
assert_eq!(r, "Hi");
assert_eq!(r2, "Hi");
}String ist im Wesentlichen ein „besseres &str mit Heap-Allocation" — und die Deref-Implementierung macht das transparent.
Box, Rc, Arc
use std::rc::Rc;
fn main() {
let b: Box<i32> = Box::new(42);
let r: &i32 = &*b; // Box dereferenzieren
assert_eq!(*r, 42);
assert_eq!(*b, 42); // direkt — auch ok
let rc: Rc<String> = Rc::new(String::from("Hi"));
assert_eq!(rc.len(), 2); // Auto-Deref durch Rc, dann String → str
}Box<T> und Rc<T> implementieren Deref<Target = T>. Du kannst sie wie normale Referenzen behandeln — der Compiler ruft die deref-Methode automatisch auf.
Deref-Coercion bei Funktions-Argumenten
Eine der mächtigsten Anwendungen: bei Funktions-Calls dereferenziert der Compiler automatisch, wenn das den Typ passend macht.
fn drucken(s: &str) {
println!("{s}");
}
fn main() {
let owned = String::from("Hi");
drucken(&owned); // &String → &str (Coercion)
drucken("Literal"); // &str — direkt
drucken(&"Literal".to_string()); // &String → &str
}Die Funktion erwartet &str. Aufrufer gibt &String. Der Compiler erkennt: String: Deref<Target = str> — also kann &String zu &str gecoerced werden. Eine kostenlose Konvertierung im Maschinencode (&str ist nur ein Pointer auf die Bytes plus Länge — beide schon in String vorhanden).
Was alles coerciert wird
| Von | Nach | Möglich, wenn |
|---|---|---|
&T | &U | T: Deref<Target = U> |
&mut T | &mut U | T: DerefMut<Target = U> |
&mut T | &U | T: Deref<Target = U> |
Die Regel ist transitiv. &String kann zu &str werden, &Box<String> zu &str (zwei Deref-Schritte), und so weiter.
DerefMut für mutable Coercion
DerefMut ist das Pendant für &mut:
fn mutieren(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut b: Box<String> = Box::new(String::from("Hi"));
mutieren(&mut b); // &mut Box<String> → &mut String
println!("{b}"); // "Hi!"
}Box<T>: DerefMut<Target = T> — also &mut Box<String> → &mut String.
Wann Auto-Deref NICHT greift
Drei Fälle:
1. Assignment links vom =
fn main() {
let mut x = 5;
let r = &mut x;
// r = 10; // Fehler — würde die Referenz neu binden
*r = 10; // ok — schreibt in den Wert
}Bei r = ... will der Compiler die Bindung r neu setzen, nicht den Wert hinter r. Für Mutation des Werts: *r = ....
2. Vergleich von Referenzen
fn main() {
let a = 5;
let b = 5;
let ra = &a;
let rb = &b;
// Bei == arbeiten beide Operanden auf demselben Level
assert_eq!(ra, rb); // Vergleicht die i32-Werte (Auto-Deref)
assert_eq!(*ra, *rb); // identisch — explizit
}Hier funktioniert == durch den PartialEq-Impl für &i32, der intern vergleicht.
3. Generische Funktionen mit Trait-Bounds
use std::fmt::Display;
fn drucke<T: Display>(x: T) {
println!("{x}");
}
fn main() {
let s = String::from("Hi");
drucke(&s); // T = &String — Display ist für &String impl
drucke(s); // T = String — Display ist für String impl
}Bei generischen Bounds wird der konkrete Typ exakt geprüft. Deref-Coercion findet hier nicht automatisch statt (außer der Trait-Implementierung).
Wichtige Deref-Implementierungen in der Stdlib
| Typ | Deref-Target | Praxis-Bedeutung |
|---|---|---|
String | str | &String → &str |
Vec<T> | [T] | &Vec<T> → &[T] |
Box<T> | T | Smart-Pointer wirkt wie Referenz |
Rc<T> | T | Reference-Counted, transparenter Zugriff |
Arc<T> | T | Atomic-Rc, gleichermaßen transparent |
Cow<'a, T> | T | Borrowed/Owned wirkt wie Borrow |
MutexGuard<T> | T | Mutex-Guard wirkt wie &mut T |
RefMut<T> | T | RefCell-Mutable wirkt wie &mut T |
PathBuf | Path | analog zu String/str |
OsString | OsStr | analog |
CString | CStr | analog |
Alle diese Deref-Beziehungen machen die jeweils kompaktere Form (&str, &[T], &T, ...) zur idiomatischen Funktions-Signatur. Owner-Typen werden automatisch übergeben.
Custom Deref-Impl
Für eigene Smart-Pointer kannst du Deref implementieren:
use std::ops::Deref;
struct MeinBox<T>(T);
impl<T> Deref for MeinBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let b = MeinBox(String::from("Hi"));
println!("{}", b.len()); // Auto-Deref: b → &String → &str → len()
let r: &str = &b; // Coercion
println!("{r}");
}Eine eigene Box-artige Struktur — durch Deref wirkt sie nach außen wie eine Referenz auf den inneren Wert.
Vorsicht bei Deref-Misuse
Deref ist kein „beliebiger Konvertierungs-Operator". Die Rust API Guidelines sagen klar: Deref nur für Smart-Pointer, nicht für „dieser Typ verhält sich ein bisschen wie der andere". Wer Deref missbraucht, schafft verwirrende APIs (z. B. wenn eine Methode überraschend durch Deref-Coercion gefunden wird).
Praxis: Dereferenzierung im echten Code
Library-API mit &str statt &String
pub fn ist_email(s: &str) -> bool {
s.contains('@')
}
fn main() {
let lit = "user@host";
let owned = String::from("alice@example.com");
assert!(ist_email(lit)); // &str direkt
assert!(ist_email(&owned)); // &String → &str via Deref-Coercion
}&str als Parameter ist flexibler als &String — beides funktioniert dank Deref-Coercion. Clippy warnt bei &String-Parametern (clippy::ptr_arg).
Slice-API mit &[T] statt &Vec<T>
pub fn summe(v: &[i32]) -> i32 {
v.iter().sum()
}
fn main() {
let owned = vec![1, 2, 3];
let array = [10, 20, 30];
assert_eq!(summe(&owned), 6); // &Vec<i32> → &[i32]
assert_eq!(summe(&array), 60); // &[i32; 3] → &[i32]
assert_eq!(summe(&owned[1..]), 5); // Sub-Slice direkt
}&[T] als Parameter akzeptiert Vec, Array und Sub-Slice. Drei Aufrufer-Varianten, eine Funktion.
Method-Calls über Box
struct Datenbank {
verbindungen: Vec<String>,
}
impl Datenbank {
fn anzahl(&self) -> usize {
self.verbindungen.len()
}
}
fn main() {
let db: Box<Datenbank> = Box::new(Datenbank {
verbindungen: vec!["a".into(), "b".into()],
});
assert_eq!(db.anzahl(), 2); // db.anzahl() funktioniert durch Auto-Deref
}db: Box<Datenbank> lässt sich wie Datenbank benutzen — alle Methoden funktionieren transparent.
Cow als Funktions-Parameter
use std::borrow::Cow;
fn ausgabe(s: Cow<str>) {
println!("{s}"); // Cow<str> → &str via Deref
}
fn main() {
ausgabe(Cow::Borrowed("Hi"));
ausgabe(Cow::Owned(String::from("Welt")));
}Cow<str> dereferenziert zu &str. Sowohl borrowed als auch owned Variante werden im Aufruf transparent behandelt.
MutexGuard transparent
use std::sync::Mutex;
fn main() {
let m = Mutex::new(vec![1, 2, 3]);
{
let mut guard = m.lock().unwrap();
guard.push(4); // Auto-Deref: MutexGuard → Vec<i32>
guard.push(5);
assert_eq!(guard.len(), 5);
}
}MutexGuard<Vec<i32>> dereferenziert (über DerefMut) zu Vec<i32>. Du kannst guard.push(...) direkt aufrufen — kein expliziter * nötig.
Path-Argument mit AsRef
use std::path::Path;
pub fn datei_existiert<P: AsRef<Path>>(p: P) -> bool {
p.as_ref().exists()
}
fn main() {
let s = String::from("/etc/hosts");
assert!(datei_existiert(&s)); // &String → &Path via AsRef
assert!(datei_existiert("/etc/hosts"));
assert!(datei_existiert(Path::new("/etc/hosts")));
}AsRef<Path> ist eine Trait-basierte Variante von Deref-Coercion — flexibler bei Cross-Type-Konvertierungen. Klassisch in std::fs-APIs.
Rc für geteilten Zugriff
use std::rc::Rc;
struct Konfig { host: String, port: u16 }
fn host_von(k: &Konfig) -> &str {
&k.host
}
fn main() {
let cfg = Rc::new(Konfig { host: "localhost".into(), port: 8080 });
let cfg2 = Rc::clone(&cfg);
println!("{}", host_von(&cfg)); // &Rc<Konfig> → &Konfig
println!("{}", host_von(&cfg2));
}&Rc<Konfig> wird durch Deref-Coercion zu &Konfig. Die Funktion host_von kennt den Smart-Pointer nicht — die Coercion ist transparent.
Eigener Smart-Pointer
use std::ops::Deref;
// Smart-Pointer, der mitzählt, wie oft dereferenziert wurde
struct Counted<T> {
wert: T,
count: std::cell::Cell<u64>,
}
impl<T> Deref for Counted<T> {
type Target = T;
fn deref(&self) -> &T {
self.count.set(self.count.get() + 1);
&self.wert
}
}
fn main() {
let c = Counted { wert: String::from("Hi"), count: std::cell::Cell::new(0) };
println!("{}", c.len()); // 1
println!("{}", c.chars().count()); // 2
println!("Zugriffe: {}", c.count.get());
}Ein Smart-Pointer, der jeden Method-Call zählt. Klassisches Lehrbeispiel für Custom Deref.
Format-Helper mit Deref
fn ausgeben(text: &str) {
println!("[{:>10}]", text);
}
fn main() {
let owned = String::from("Hallo");
ausgeben(&owned); // &String → &str
ausgeben("Direkt");
ausgeben(&"Konvertiert".to_string());
}Eine &str-API akzeptiert dank Auto-Deref jede Form, die zu einem &str coerciert werden kann.
FAQ
Wann brauche ich * explizit?
Bei Mutation eines Werts durch eine &mut-Referenz (*r = neuer_wert), beim Vergleich zweier Werte hinter Referenzen (manchmal), und in seltenen Fällen, in denen der Compiler den Typ nicht eindeutig auflösen kann. In allen anderen Fällen reicht Auto-Deref.
Auto-Deref greift bei Method-Calls und in vielen Coercion-Punkten.
Bei r.method() probiert der Compiler r, *r, **r etc. durch. Bei Funktions-Argumenten wird die Deref-Chain transitiv gefolgt — &Box<String> kann zu &str werden.
&str als Parameter ist idiomatischer als &String.
Beide funktionieren dank Auto-Deref am Aufruf-Ort. Aber &str akzeptiert auch String-Literale ohne Allocation. Clippy warnt mit clippy::ptr_arg bei &String-Parametern.
Box verhält sich syntaktisch wie &T.
Method-Calls, Field-Access, Vergleiche — alles funktioniert auf Box<T> so, als wäre es ein T. Das ist die zentrale Eigenschaft eines Smart-Pointers in Rust.
Deref nur für Smart-Pointer implementieren.
Die Rust API Guidelines verbieten Deref als allgemeinen Konvertierungs-Operator. Es ist für „dieser Typ ist im Wesentlichen ein Wrapper um einen anderen Typ" gedacht. Für API-Konvertierungen lieber AsRef<T> oder From<T> verwenden.
DerefMut setzt Deref voraus.
Wer DerefMut implementiert, muss auch Deref implementieren. Die mutable Version ist eine Erweiterung der shared.
Method-Lookup ist clever, aber nicht magisch.
Der Compiler probiert beim r.method()-Lookup: r, &r, &mut r, *r, *&*r etc. — wenn eine passende Methode gefunden wird, nimmt er sie. Bei mehreren passenden gibt es Ambiguität — selten, aber löseware mit expliziter Type-Notation.
Auto-Deref macht keine Allocation.
&String → &str ist im Maschinencode oft eine 0-Instruktion-Operation (oder ein Register-Move). Der String hat schon Pointer + Länge — ein &str ist genau das.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Smart Pointers and Deref
- std::ops::Deref
- std::ops::DerefMut
- Rust Reference – Deref coercion
- Rust API Guidelines – Smart Pointers