Der impl-Block ist der Ort, an dem Methoden zu einem Struct kommen. Anders als bei OOP-Klassen sind Daten (Struct) und Verhalten (impl) syntaktisch getrennt. Das hat einen erheblichen Vorteil: du kannst mehrere impl-Blöcke pro Typ haben — gruppiert nach Verantwortlichkeit, je nach Trait-Implementierung. Dieser Artikel zerlegt die drei Receiver-Typen (&self, &mut self, self), zeigt das Method-Resolution-System, klärt den Unterschied zwischen inherent und trait methods und führt durch typische Patterns wie Method-Chaining und Self-Returns.
Die Grundform
struct Konto {
saldo_cent: i64,
}
impl Konto {
fn neu() -> Self {
Konto { saldo_cent: 0 }
}
fn einzahlen(&mut self, cent: i64) {
self.saldo_cent += cent;
}
fn saldo(&self) -> i64 {
self.saldo_cent
}
}Aufbau:
impl TypName { ... }— der Block.- Methoden — Funktionen mit
self-Receiver. Aufruf:instanz.methode(...). - Associated Functions — Funktionen ohne
self. Aufruf:TypName::funktion(...).
Mehr zu Associated Functions im eigenen Artikel.
Die drei Receiver-Typen
&self — lesender Zugriff
# struct Konto { saldo_cent: i64 }
impl Konto {
fn saldo(&self) -> i64 {
self.saldo_cent
}
fn ist_im_minus(&self) -> bool {
self.saldo_cent < 0
}
}&self ist die häufigste Form. Bedeutung:
- Die Methode liest das Objekt, mutiert nichts.
- Aufrufer behält Ownership; das Objekt bleibt nach dem Call verwendbar.
- Mehrere
&self-Calls können parallel laufen (an verschiedenen Borrows der gleichen Instanz).
&mut self — schreibender Zugriff
# struct Konto { saldo_cent: i64 }
impl Konto {
fn einzahlen(&mut self, cent: i64) {
self.saldo_cent += cent;
}
fn zuruecksetzen(&mut self) {
self.saldo_cent = 0;
}
}&mut self:
- Die Methode mutiert das Objekt.
- Die Bindung im Aufrufer muss
mutsein:let mut k = Konto::neu(); k.einzahlen(100);. - Während des Calls gibt es keinen anderen Borrow auf das Objekt.
self — verbrauchender Zugriff
# struct Konto { saldo_cent: i64 }
impl Konto {
fn schliessen(self) -> i64 {
self.saldo_cent
}
}
fn main() {
let k = Konto { saldo_cent: 5000 };
let endwert = k.schliessen();
// k.einzahlen(100); // Fehler — k wurde verbraucht
println!("{endwert}");
}self:
- Die Methode verbraucht das Objekt — nach dem Call existiert es nicht mehr.
- Klassisch für „Abschluss"-Methoden wie
bauen()bei Buildern oderinto_inner()bei Wrappern. - Auch sinnvoll, wenn die Methode das Objekt transformiert und ein anderes zurückgibt.
Wann welcher Receiver?
| Situation | Wahl |
|---|---|
| Nur Felder lesen | &self |
| Felder modifizieren | &mut self |
Objekt in anderen umwandeln (bauen, into) | self |
| Objekt vollständig „verbrauchen" | self |
| Builder-Methode mit Chain | mut self → Self |
| Methode ohne Instanz | kein self (Associated Function) |
Faustregel: default &self, &mut self wenn nötig, self nur wenn das Objekt wirklich verbraucht werden soll.
Self vs. self
Zwei verwandte, aber unterschiedliche Konzepte:
self(klein) — der Receiver-Parameter. Eine Instanz des Typs.Self(groß) — der Typ selbst. Alias für den Struct-Namen.
# struct Konto { saldo_cent: i64 }
impl Konto {
// Self ist Alias für Konto:
fn neu() -> Self {
Self { saldo_cent: 0 } // identisch zu Konto { saldo_cent: 0 }
}
fn kopie(&self) -> Self {
Self { saldo_cent: self.saldo_cent }
}
}Self ist sehr nützlich:
- Klarer als der Typ-Name selbst.
- Funktioniert in generischen Implementierungen, wo der konkrete Name unklar ist.
- Pflicht in Trait-Definitionen.
Mehrere impl-Blöcke
Du kannst mehrere impl-Blöcke für denselben Typ haben. Das ist nützlich, um Methoden thematisch zu gruppieren:
struct User { id: u64, name: String, email: String }
// Konstruktoren und Basis-Operations
impl User {
fn neu(id: u64, name: String, email: String) -> Self {
User { id, name, email }
}
}
// Validierung
impl User {
fn ist_valider_name(&self) -> bool {
!self.name.is_empty() && self.name.len() < 50
}
fn ist_valider_email(&self) -> bool {
self.email.contains('@')
}
}
// Anzeige-Helfer
impl User {
fn anzeige_name(&self) -> String {
format!("#{} {}", self.id, self.name)
}
}Funktional identisch zu einem einzelnen großen impl-Block, aber organisatorisch klarer. Manchmal werden mehrere Blöcke auch über Module verteilt — z. B. ein Basis-impl im selben Modul wie der Struct, weitere impls in anderen Modulen.
Method Resolution — wie der Compiler die Methode findet
Wenn du instance.methode() aufrufst, geht der Compiler folgende Liste durch:
- Inherent method auf dem Typ direkt (
impl T { fn methode(...) }). - Inherent method auf
&Toder&mut T(über Auto-Borrow). - Trait method auf einem importierten Trait, der für
Timplementiert ist. - Auto-Deref: wenn
T: Deref<Target = U>, wiederhole fürU.
Das macht Methoden-Aufrufe sehr flexibel:
fn main() {
let s = String::from("Hallo");
// s.len() — Methode auf &str via Auto-Deref + Auto-Borrow
let n = s.len();
println!("{n}");
// Box<String>::len() — gleiches Spiel, eine Ebene tiefer
let b: Box<String> = Box::new(String::from("Welt"));
let m = b.len();
println!("{m}");
}b.len() macht:
bistBox<String>.Box<T>: Deref<Target = T>— Deref zuString.String: Deref<Target = str>— Deref zustr.str::len(&str) -> usize— Auto-Borrow wendet&an.
Eine Method-Call-Stelle, viele Stufen Auto-Deref/Auto-Borrow. Komplett transparent.
Inherent vs. Trait-Methoden
Es gibt zwei Arten von Methoden:
Inherent Methods
Methoden, die direkt am Typ definiert sind, ohne Trait-Vermittlung:
# struct Konto { saldo_cent: i64 }
impl Konto {
fn saldo(&self) -> i64 { self.saldo_cent } // inherent
}Trait-Methods
Methoden, die über einen Trait kommen:
# struct Konto { saldo_cent: i64 }
use std::fmt;
impl fmt::Display for Konto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.saldo_cent)
}
}
fn main() {
let k = Konto { saldo_cent: 5000 };
println!("{k}"); // ruft die Trait-Methode
}Beide funktionieren mit derselben Syntax am Aufruf-Ort. Im Code-Browse-Tool sind sie aber unterscheidbar:
- Inherent — direkt am Typ.
- Trait — am Trait, der für den Typ implementiert ist.
Wenn beide Namens-Identische Methoden bereitstellen, hat inherent Vorrang. Für die Trait-Variante explizit: <MyType as MyTrait>::method(&instance).
Method-Chaining mit Self-Returns
Klassisches Pattern für flüssige APIs:
# struct Konto { saldo_cent: i64 }
impl Konto {
fn neu() -> Self { Konto { saldo_cent: 0 } }
fn einzahlen(mut self, cent: i64) -> Self {
self.saldo_cent += cent;
self
}
fn abheben(mut self, cent: i64) -> Self {
self.saldo_cent -= cent;
self
}
}
fn main() {
let k = Konto::neu()
.einzahlen(5000)
.einzahlen(3000)
.abheben(2000);
assert_eq!(k.saldo_cent, 6000);
}Bei diesem Stil nimmt jede Methode mut self (Move + Mutation) und gibt Self zurück. Der Aufrufer kettelt — keine mut-Bindungen außen nötig.
Alternative mit &mut self:
# struct Konto { saldo_cent: i64 }
impl Konto {
fn einzahlen(&mut self, cent: i64) -> &mut Self {
self.saldo_cent += cent;
self
}
}
fn main() {
let mut k = Konto { saldo_cent: 0 };
k.einzahlen(100).einzahlen(200).einzahlen(300);
assert_eq!(k.saldo_cent, 600);
}&mut Self-Returns sind weniger verbreitet, aber funktionieren ähnlich.
Praxis: impl-Blöcke im echten Code
Domain-Service
use std::collections::HashMap;
pub struct UserService {
users: HashMap<u64, String>,
naechste_id: u64,
}
impl UserService {
pub fn neu() -> Self {
UserService { users: HashMap::new(), naechste_id: 1 }
}
pub fn registrieren(&mut self, name: String) -> u64 {
let id = self.naechste_id;
self.naechste_id += 1;
self.users.insert(id, name);
id
}
pub fn finden(&self, id: u64) -> Option<&str> {
self.users.get(&id).map(|s| s.as_str())
}
pub fn anzahl(&self) -> usize {
self.users.len()
}
}Klassische Service-Struktur: Konstruktor, Mutator, Read-Only-Methoden. Klare Receiver-Wahl pro Methode.
Math-Type mit vielen Operationen
#[derive(Clone, Copy, Debug)]
pub struct Vec3 { pub x: f64, pub y: f64, pub z: f64 }
impl Vec3 {
pub const NULL: Vec3 = Vec3 { x: 0.0, y: 0.0, z: 0.0 };
pub fn neu(x: f64, y: f64, z: f64) -> Self { Vec3 { x, y, z } }
pub fn laenge(&self) -> f64 {
(self.x*self.x + self.y*self.y + self.z*self.z).sqrt()
}
pub fn normiert(self) -> Self {
let l = self.laenge();
if l == 0.0 { return Self::NULL; }
Vec3::neu(self.x/l, self.y/l, self.z/l)
}
pub fn skalarprodukt(self, other: Vec3) -> f64 {
self.x*other.x + self.y*other.y + self.z*other.z
}
pub fn addieren(&mut self, other: Vec3) {
self.x += other.x;
self.y += other.y;
self.z += other.z;
}
}Drei Receiver-Typen in einem Struct: &self für Lesen, &mut self für In-Place-Mutation, self für Transformationen (normiert).
Buffered Writer
pub struct Buffer {
daten: Vec<u8>,
kapazitaet: usize,
}
impl Buffer {
pub fn neu(kapazitaet: usize) -> Self {
Buffer { daten: Vec::with_capacity(kapazitaet), kapazitaet }
}
pub fn schreiben(&mut self, bytes: &[u8]) -> Result<(), &'static str> {
if self.daten.len() + bytes.len() > self.kapazitaet {
return Err("Buffer voll");
}
self.daten.extend_from_slice(bytes);
Ok(())
}
pub fn als_slice(&self) -> &[u8] {
&self.daten
}
pub fn in_vec(self) -> Vec<u8> {
self.daten
}
}in_vec(self) ist eine konsumierende Methode — sie übergibt den internen Vec an den Aufrufer. Der Buffer existiert danach nicht mehr.
Counter mit Chain-API
pub struct Counter {
wert: u64,
}
impl Counter {
pub fn neu() -> Self { Counter { wert: 0 } }
pub fn inkrementieren(mut self) -> Self {
self.wert += 1;
self
}
pub fn um(mut self, n: u64) -> Self {
self.wert += n;
self
}
pub fn wert(self) -> u64 { self.wert }
}
fn main() {
let c = Counter::neu()
.inkrementieren()
.um(5)
.inkrementieren()
.wert();
assert_eq!(c, 7);
}Self-Return-Chain mit wert() als finale, konsumierende Methode.
Tagebuch-Pattern mit mehreren impl-Blöcken
pub struct Tagebuch {
eintraege: Vec<String>,
}
// Konstruktoren
impl Tagebuch {
pub fn leer() -> Self { Tagebuch { eintraege: Vec::new() } }
pub fn mit_eintraegen(eintraege: Vec<String>) -> Self {
Tagebuch { eintraege }
}
}
// Modifikation
impl Tagebuch {
pub fn eintragen(&mut self, text: String) {
self.eintraege.push(text);
}
pub fn leeren(&mut self) {
self.eintraege.clear();
}
}
// Abfrage
impl Tagebuch {
pub fn anzahl(&self) -> usize { self.eintraege.len() }
pub fn ist_leer(&self) -> bool { self.eintraege.is_empty() }
pub fn letzter(&self) -> Option<&str> {
self.eintraege.last().map(|s| s.as_str())
}
}Mehrere impl-Blöcke nach Verantwortlichkeit gruppiert. Funktional identisch zu einem großen Block, aber besser lesbar.
Generische Methode mit Trait-Bound
pub struct Container<T> {
items: Vec<T>,
}
impl<T> Container<T> {
pub fn neu() -> Self {
Container { items: Vec::new() }
}
pub fn hinzufuegen(&mut self, item: T) {
self.items.push(item);
}
}
impl<T: Clone> Container<T> {
pub fn klonen(&self) -> Vec<T> {
self.items.clone()
}
}Ein generischer impl<T>-Block für alle T. Ein zweiter impl<T: Clone>-Block nur für klonbare T. So lassen sich Methoden auf bestimmte Trait-Constraints einschränken.
State-Mutator mit Closure-Callback
pub struct Setting<T: Clone> {
wert: T,
on_change: Vec<Box<dyn Fn(&T)>>,
}
impl<T: Clone> Setting<T> {
pub fn neu(initial: T) -> Self {
Setting { wert: initial, on_change: Vec::new() }
}
pub fn setzen(&mut self, neu: T) {
self.wert = neu;
for callback in &self.on_change {
callback(&self.wert);
}
}
pub fn beobachten(&mut self, callback: Box<dyn Fn(&T)>) {
self.on_change.push(callback);
}
pub fn wert(&self) -> &T { &self.wert }
}Observer-Pattern in Rust — Closures als Callbacks. Box<dyn Fn(&T)> für heterogene Callback-Sammlung.
Trait-Methoden für Library-Typen
use std::fmt;
pub struct Geld { eur: u32, cent: u8 }
impl Geld {
pub fn neu(eur: u32, cent: u8) -> Self {
Geld { eur, cent: cent.min(99) }
}
}
impl fmt::Display for Geld {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{},{:02} €", self.eur, self.cent)
}
}
fn main() {
let g = Geld::neu(19, 90);
println!("{g}"); // "19,90 €"
let als_string: String = g.to_string(); // via Display → ToString
assert_eq!(als_string, "19,90 €");
}Display-Trait gibt println!-Format. Außerdem bekommt der Typ automatisch ToString (über die Blanket-Impl in der Stdlib).
Verkettete Methoden mit &mut self-Return
pub struct UrlBuilder {
schema: String,
host: String,
pfad: String,
params: Vec<(String, String)>,
}
impl UrlBuilder {
pub fn neu(host: impl Into<String>) -> Self {
UrlBuilder {
schema: "https".into(),
host: host.into(),
pfad: "/".into(),
params: Vec::new(),
}
}
pub fn pfad(&mut self, p: &str) -> &mut Self {
self.pfad = p.to_string();
self
}
pub fn param(&mut self, key: &str, wert: &str) -> &mut Self {
self.params.push((key.into(), wert.into()));
self
}
pub fn bauen(&self) -> String {
let query = if self.params.is_empty() {
String::new()
} else {
let pairs: Vec<String> = self.params.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
format!("?{}", pairs.join("&"))
};
format!("{}://{}{}{}", self.schema, self.host, self.pfad, query)
}
}
fn main() {
let mut b = UrlBuilder::neu("api.example.com");
b.pfad("/users").param("page", "1").param("limit", "20");
let url = b.bauen();
println!("{url}");
}&mut self-Return ermöglicht Chaining ohne Move. Builder bleibt nach der Chain noch verfügbar (für weitere Konfiguration oder bauen()).
Interessantes
Default-Receiver ist &self.
Wenn deine Methode den State nicht ändert, ist &self die richtige Wahl. Aufrufer kann das Objekt während des Calls auch in anderen Borrows verwenden. Mehrere &self-Calls können parallel laufen.
&mut self bedeutet exklusiver Zugriff.
Während eines &mut self-Calls hat niemand sonst eine Referenz auf das Objekt. Der Borrow Checker garantiert das. Damit ist Mutation atomar im Sinne von Aliasing — keine Race Conditions.
self als Receiver verbraucht das Objekt.
Methoden mit self (ohne &) übernehmen Ownership. Nach dem Call existiert das Objekt nicht mehr. Klassisch für Builder-bauen(), Wrapper-into_inner(), oder Transformationen, die einen neuen Typ liefern.
Self ist der Typ-Alias innerhalb von impl.
Self (großgeschrieben) ersetzt den konkreten Typ-Namen. Sehr nützlich in generischen Implementierungen, in Trait-Definitionen, und einfach für Lesbarkeit. fn neu() -> Self ist idiomatischer als fn neu() -> Konto.
Mehrere impl-Blöcke sind erlaubt.
Du darfst beliebig viele impl Konto { ... }-Blöcke haben. Sie werden vom Compiler zusammengefasst. Nützlich für Gruppierung nach Verantwortlichkeit oder wenn Methoden über mehrere Module verteilt sind.
Method-Resolution probiert Auto-Borrow und Auto-Deref.
Der Compiler probiert instance.methode() als (&instance).methode(), (&mut instance).methode(), (*instance).methode() etc. — die erste passende Variante gewinnt. Das macht Aufrufe über Smart-Pointer und Referenzen transparent.
Inherent-Methoden haben Vorrang vor Trait-Methoden.
Wenn ein Inherent-Method und eine Trait-Method denselben Namen haben, gewinnt der Inherent-Method. Für die Trait-Variante explizit aufrufen: <MyType as MyTrait>::method(&instance).
Builder-Pattern: self + -> Self für Chain.
Klassisches Pattern: fn name(mut self, ...) -> Self ermöglicht obj.name(a).name(b).name(c) — Chaining ohne mut-Bindung im Aufrufer. Die finale Methode bauen(self) verbraucht den Builder.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Method Syntax
- Rust Reference – Implementations
- Rust Reference – Method-Call Expressions
- Rust API Guidelines – Naming