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

Rust impl-Block
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

Rust Read-Only-Methode
# 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

Rust Mutating-Methode
# 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 mut sein: let mut k = Konto::neu(); k.einzahlen(100);.
  • Während des Calls gibt es keinen anderen Borrow auf das Objekt.

self — verbrauchender Zugriff

Rust Konsumierende Methode
# 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 oder into_inner() bei Wrappern.
  • Auch sinnvoll, wenn die Methode das Objekt transformiert und ein anderes zurückgibt.

Wann welcher Receiver?

SituationWahl
Nur Felder lesen&self
Felder modifizieren&mut self
Objekt in anderen umwandeln (bauen, into)self
Objekt vollständig „verbrauchen"self
Builder-Methode mit Chainmut selfSelf
Methode ohne Instanzkein 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.
Rust Self großgeschrieben
# 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:

Rust Gruppierung
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:

  1. Inherent method auf dem Typ direkt (impl T { fn methode(...) }).
  2. Inherent method auf &T oder &mut T (über Auto-Borrow).
  3. Trait method auf einem importierten Trait, der für T implementiert ist.
  4. Auto-Deref: wenn T: Deref<Target = U>, wiederhole für U.

Das macht Methoden-Aufrufe sehr flexibel:

Rust Auto-Borrow + Auto-Deref
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:

  • b ist Box<String>.
  • Box<T>: Deref<Target = T> — Deref zu String.
  • String: Deref<Target = str> — Deref zu str.
  • 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:

Rust Inherent
# struct Konto { saldo_cent: i64 }
impl Konto {
    fn saldo(&self) -> i64 { self.saldo_cent }   // inherent
}

Trait-Methods

Methoden, die über einen Trait kommen:

Rust Trait-Method
# 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:

Rust Method-Chain
# 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:

Rust &mut self mit Self-Return
# 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

Rust UserService
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

Rust Vec3
#[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

Rust 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

Rust Fluent-Counter
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

Rust Tagebuch
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

Rust Container mit Generic
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

Rust On-Change-Pattern
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

Rust Display-Impl
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

Rust Builder mit mut
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

/ Weiter

Zurück zu Structs & Methoden

Zur Übersicht