Rust hat keine spezielle Operator-Overloading-Syntax — Operatoren sind nichts anderes als Trait-Methoden. a + b ist tatsächlich a.add(b), also Add::add(a, b). Wenn du + für deinen Typ definieren willst, implementierst du std::ops::Add. Das gleiche gilt für - (Sub), * (Mul), [idx] (Index), == (PartialEq) und alle anderen Operatoren. Dieses Pattern ist konsistent, ergonomisch und erlaubt mathematische Typen (Vektoren, Matrizen, komplexe Zahlen), Domain-spezifische APIs (z.B. Pfad-Konkatenation mit /), und ergonomische Container-APIs.

Add — der erste Operator

std::ops::Add ist das Standard-Beispiel für Operator-Overloading.

Rust Vektor-Addition
use std::ops::Add;

#[derive(Debug, Copy, Clone)]
struct Vektor2D {
    x: f64,
    y: f64,
}

impl Add for Vektor2D {
    type Output = Vektor2D;

    fn add(self, other: Vektor2D) -> Vektor2D {
        Vektor2D {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let v1 = Vektor2D { x: 1.0, y: 2.0 };
    let v2 = Vektor2D { x: 3.0, y: 4.0 };
    let v3 = v1 + v2;        // ruft v1.add(v2)
    println!("{v3:?}");      // Vektor2D { x: 4.0, y: 6.0 }
}

impl Add for Vektor2D definiert die +-Operation. Beachte:

  • type Output = Vektor2D; — der Result-Typ (Associated Type)
  • fn add(self, other: Vektor2D) -> Vektor2D — die eigentliche Methode
  • self (nicht &self!) — die Operation konsumiert die Operanden

Das self-by-value ist wichtig: die Add-Operation darf den Operanden bewegen. Bei Copy-Typen wie Vektor2D (mit Copy-derive) ist das transparent — sonst musst du &Vektor2D + &Vektor2D separat implementieren (siehe später).

Add mit anderem Rhs-Typ

Add hat zwei Generic-Parameter: Add<Rhs = Self>. Der Rhs-Parameter ist standardmäßig gleich Self, kann aber explizit anders gewählt werden.

Rust Add mit Scalar
use std::ops::Add;

#[derive(Debug, Copy, Clone)]
struct Vektor2D { x: f64, y: f64 }

// Vektor + Vektor → Vektor
impl Add for Vektor2D {
    type Output = Vektor2D;
    fn add(self, other: Vektor2D) -> Vektor2D {
        Vektor2D { x: self.x + other.x, y: self.y + other.y }
    }
}

// Vektor + f64 → Vektor (Scalar-Addition)
impl Add<f64> for Vektor2D {
    type Output = Vektor2D;
    fn add(self, scalar: f64) -> Vektor2D {
        Vektor2D { x: self.x + scalar, y: self.y + scalar }
    }
}

fn main() {
    let v = Vektor2D { x: 1.0, y: 2.0 };
    let v2 = v + Vektor2D { x: 1.0, y: 1.0 };   // Vektor + Vektor
    let v3 = v + 10.0;                           // Vektor + f64
    println!("{v2:?}, {v3:?}");
}

Zwei verschiedene Add-Implementations: einmal mit Self (Vektor-Vektor), einmal mit f64 (Vektor-Scalar). Der Compiler wählt anhand der konkreten Operand-Typen.

Beachte: v + 10.0 funktioniert, 10.0 + v aber nicht — das wäre impl Add<Vektor2D> for f64, was wir nicht implementiert haben (und wegen Orphan-Rule auch nicht ohne Newtype implementieren dürften).

Die wichtigsten Operator-Traits

OperatorTraitMethode
+Addadd
-Subsub
*Mulmul
/Divdiv
%Remrem
-x (unär)Negneg
+=AddAssignadd_assign
-=SubAssignsub_assign
*=MulAssignmul_assign
/=DivAssigndiv_assign
== / !=PartialEqeq / ne
<, >, <=, >=PartialOrdlt, gt, le, ge
&, |, ^BitAnd, BitOr, BitXorbitand, bitor, bitxor
<<, >>Shl, Shrshl, shr
[index]Index / IndexMutindex / index_mut
*x (Dereferenzierung)Deref / DerefMutderef / deref_mut
!Notnot
.., ..=Range-Typen(intern)

Alle in std::ops (außer PartialEq/PartialOrd, die in std::cmp sind).

AddAssign — für +=

+= ist eine eigene Operation: a += b ist AddAssign::add_assign(&mut a, b) — nicht a = a + b.

Rust AddAssign
use std::ops::AddAssign;

#[derive(Debug)]
struct Counter { value: u32 }

impl AddAssign<u32> for Counter {
    fn add_assign(&mut self, rhs: u32) {
        self.value += rhs;
    }
}

fn main() {
    let mut c = Counter { value: 0 };
    c += 5;
    c += 10;
    assert_eq!(c.value, 15);
}

add_assign nimmt &mut self und modifiziert in-place. Das ist effizienter als Add + Zuweisung — kein neues Objekt wird erzeugt.

Die Add- und AddAssign-Traits sind unabhängig: du kannst eines ohne das andere implementieren. Konvention: implementiere beide, wenn sinnvoll. Das #[derive]-Makro hilft nicht — diese musst du manuell schreiben.

PartialEq — der Vergleich

== und != kommen aus PartialEq.

Rust PartialEq
struct Point { x: i32, y: i32 }

impl PartialEq for Point {
    fn eq(&self, other: &Point) -> bool {
        self.x == other.x && self.y == other.y
    }
    // !=  via Default-Methode: !self.eq(other)
}

fn main() {
    let a = Point { x: 1, y: 2 };
    let b = Point { x: 1, y: 2 };
    let c = Point { x: 3, y: 4 };
    assert!(a == b);
    assert!(a != c);
}

PartialEq ist meist via #[derive(PartialEq)] ableitbar — der Compiler erzeugt Feld-für-Feld-Vergleich. Manuelle Implementation nur bei spezieller Logik (z.B. case-insensitive String-Vergleich).

Beachte den Namen: PartialEq — das bedeutet, dass nicht alle Werte vergleichbar sein müssen. f64::NAN == f64::NAN ist false — NaN ist mit sich selbst nicht gleich. Daher ist f64 PartialEq, aber nicht Eq (Total-Equality). Eq fordert Reflexivität: a == a für jeden Wert.

Index — [idx]-Syntax

std::ops::Index erlaubt value[idx]-Syntax für eigene Container.

Rust Index
use std::ops::Index;

struct Matrix3x3 {
    data: [[f64; 3]; 3],
}

impl Index<(usize, usize)> for Matrix3x3 {
    type Output = f64;

    fn index(&self, (row, col): (usize, usize)) -> &f64 {
        &self.data[row][col]
    }
}

fn main() {
    let m = Matrix3x3 {
        data: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]],
    };
    assert_eq!(m[(1, 1)], 5.0);
    assert_eq!(m[(0, 2)], 3.0);
}

Index<(usize, usize)> mit Tuple-Index. Klassisches Pattern für Matrix-APIs.

Beachte: index gibt &Self::Output zurück (Referenz!). Für mutable Zugriff (m[idx] = ...) brauchst du IndexMut.

Rust IndexMut
# use std::ops::{Index, IndexMut};
# struct Matrix3x3 { data: [[f64; 3]; 3] }
# impl Index<(usize, usize)> for Matrix3x3 {
#     type Output = f64;
#     fn index(&self, (r, c): (usize, usize)) -> &f64 { &self.data[r][c] }
# }

impl IndexMut<(usize, usize)> for Matrix3x3 {
    fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut f64 {
        &mut self.data[row][col]
    }
}

fn main() {
    let mut m = Matrix3x3 {
        data: [[0.0; 3]; 3],
    };
    m[(0, 0)] = 1.0;
    m[(1, 1)] = 5.0;
    assert_eq!(m[(0, 0)], 1.0);
}

Mit IndexMut funktioniert Index-basierte Mutation.

Praxis: Operator-Overloading im echten Code

Komplexe Zahlen

Rust Komplexe Zahlen
use std::ops::{Add, Sub, Mul, Neg};

#[derive(Debug, Copy, Clone, PartialEq)]
struct Complex { re: f64, im: f64 }

impl Add for Complex {
    type Output = Complex;
    fn add(self, other: Complex) -> Complex {
        Complex { re: self.re + other.re, im: self.im + other.im }
    }
}

impl Sub for Complex {
    type Output = Complex;
    fn sub(self, other: Complex) -> Complex {
        Complex { re: self.re - other.re, im: self.im - other.im }
    }
}

impl Mul for Complex {
    type Output = Complex;
    fn mul(self, other: Complex) -> Complex {
        Complex {
            re: self.re * other.re - self.im * other.im,
            im: self.re * other.im + self.im * other.re,
        }
    }
}

impl Neg for Complex {
    type Output = Complex;
    fn neg(self) -> Complex {
        Complex { re: -self.re, im: -self.im }
    }
}

fn main() {
    let a = Complex { re: 1.0, im: 2.0 };
    let b = Complex { re: 3.0, im: 4.0 };
    println!("{:?}", a + b);
    println!("{:?}", a * b);
    println!("{:?}", -a);
}

Klassische Mathematik mit überladenen Operatoren. Add, Sub, Mul, Neg — alle Standard-Arithmetik wirkt natürlich.

Vektor-Algebra

Rust Vektor mit Skalar-Multiplikation
use std::ops::{Add, Mul};

#[derive(Debug, Copy, Clone)]
struct Vec3 { x: f64, y: f64, z: f64 }

impl Add for Vec3 {
    type Output = Vec3;
    fn add(self, o: Vec3) -> Vec3 {
        Vec3 { x: self.x + o.x, y: self.y + o.y, z: self.z + o.z }
    }
}

// Vec3 * f64 (Skalar-Multiplikation)
impl Mul<f64> for Vec3 {
    type Output = Vec3;
    fn mul(self, s: f64) -> Vec3 {
        Vec3 { x: self.x * s, y: self.y * s, z: self.z * s }
    }
}

// f64 * Vec3 — eigene Impl auf f64 nötig (Orphan-Rule!)
// Workaround: nur eine Richtung implementieren, oder Newtype nutzen.

fn main() {
    let v = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
    let scaled = v * 2.0;
    let combined = v + scaled;
    println!("{combined:?}");
}

Vektor-Algebra mit Skalar-Multiplikation. Wegen Orphan-Rule können wir nicht impl Mul<Vec3> for f64 schreiben (beide nicht-lokal aus Stdlib-Sicht — in unserem Crate ist Vec3 lokal, also wäre impl Mul<Vec3> for f64 wegen f64-Fremdheit verboten).

Pfad-Konkatenation

Rust Path-Operator
use std::ops::Div;

#[derive(Debug, Clone)]
struct Pfad { segmente: Vec<String> }

impl Div<&str> for Pfad {
    type Output = Pfad;
    fn div(mut self, segment: &str) -> Pfad {
        self.segmente.push(segment.to_string());
        self
    }
}

impl Pfad {
    fn neu(start: &str) -> Self {
        Pfad { segmente: vec![start.to_string()] }
    }
    fn render(&self) -> String {
        self.segmente.join("/")
    }
}

fn main() {
    let p = Pfad::neu("home") / "user" / "documents" / "report.pdf";
    println!("{}", p.render());     // "home/user/documents/report.pdf"
}

/ als Pfad-Konkatenation. So nutzt es z.B. die Stdlib für std::path::PathBuf mit .join() — hier eine analog elegante Operator-basierte API.

Range-artige API

Rust Range-Index
use std::ops::Index;

struct Buffer { data: Vec<u8> }

impl Index<std::ops::Range<usize>> for Buffer {
    type Output = [u8];
    fn index(&self, range: std::ops::Range<usize>) -> &[u8] {
        &self.data[range]
    }
}

fn main() {
    let buf = Buffer { data: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9] };
    let slice = &buf[2..5];
    assert_eq!(slice, &[2, 3, 4]);
}

Index<Range<usize>> für Slice-Zugriff. Stdlib-Vec hat diese Impl auch — daher funktioniert &vec[2..5].

Custom PartialEq

Rust Case-Insensitive PartialEq
struct CaseInsensitiveStr(String);

impl PartialEq for CaseInsensitiveStr {
    fn eq(&self, other: &Self) -> bool {
        self.0.to_lowercase() == other.0.to_lowercase()
    }
}

fn main() {
    let a = CaseInsensitiveStr(String::from("Hello"));
    let b = CaseInsensitiveStr(String::from("HELLO"));
    assert!(a == b);
}

Manuelle PartialEq mit Custom-Logik. Im Gegensatz zum derive-Default wird hier case-insensitive verglichen.

Bitwise-Operatoren für Flags

Rust Flag-Set
use std::ops::{BitOr, BitAnd};

#[derive(Debug, Copy, Clone, PartialEq)]
struct Flags(u32);

impl BitOr for Flags {
    type Output = Flags;
    fn bitor(self, other: Flags) -> Flags {
        Flags(self.0 | other.0)
    }
}

impl BitAnd for Flags {
    type Output = Flags;
    fn bitand(self, other: Flags) -> Flags {
        Flags(self.0 & other.0)
    }
}

const READ: Flags = Flags(0b001);
const WRITE: Flags = Flags(0b010);
const EXEC: Flags = Flags(0b100);

fn main() {
    let perms = READ | WRITE;
    println!("Has read: {}", (perms & READ).0 != 0);
    println!("Has exec: {}", (perms & EXEC).0 != 0);
}

Bitwise-Overloading für Flag-Sets. Klassisches Pattern, das z.B. die bitflags-Crate automatisiert.

Deref für Smart-Pointer

Rust Custom Smart-Pointer
use std::ops::Deref;

struct MyBox<T> { value: T }

impl<T> MyBox<T> {
    fn neu(value: T) -> Self { MyBox { value } }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.value
    }
}

fn main() {
    let b = MyBox::neu(42);
    assert_eq!(*b, 42);          // Deref macht * möglich

    let s = MyBox::neu(String::from("hello"));
    println!("len = {}", s.len());   // Deref-Coercion: String-Methoden
}

Deref für eigene Smart-Pointer. *b ruft b.deref(). Deref-Coercion erlaubt zusätzlich, dass alle Methoden des Target-Typs auf dem Smart-Pointer erscheinen.

Interessantes

Operatoren = Trait-Methoden.

a + b ist a.add(b) aus std::ops::Add. Kein Operator hat eigene Sprach-Magie — alles ist Trait-Implementation.

Add, Sub, Mul, Div, Rem — die Arithmetik.

In std::ops. Jeder hat Associated Type Output und nimmt self by value. Bei Copy-Typen transparent, bei non-Copy-Typen Move-Operation.

Add — Rhs darf anders sein.

impl Add<f64> for Vec3 erlaubt Vec3 + f64. Der Compiler wählt anhand der Operand-Typen die passende Implementation.

AddAssign / SubAssign / etc. für die Compound-Operationen.

+= ist NICHT a = a + b, sondern add_assign(&mut a, b). Eigene Trait, eigene Methode, in-place-Modifikation. Effizienter, wenn kein temporäres Objekt nötig ist.

== und != aus PartialEq.

eq ist Pflicht, ne hat Default-Implementation. Meist via #[derive(PartialEq)] ableitbar. Manuell nur bei Custom-Logik (Case-Insensitive, Approximate).

Index / IndexMut für [idx]-Syntax.

Index erlaubt Lesen, IndexMut zusätzlich Schreiben. Der Index-Typ ist Generic (usize, Tuple, Range, ...) — sehr flexibel.

Deref / DerefMut für Smart-Pointer und Newtype.

*x ruft x.deref(). Außerdem aktiviert es Deref-Coercion: Methoden des Target-Typs sind auf dem Smart-Pointer erreichbar. Bei Newtype-Wrappern aber vorsichtig nutzen — schwächt die Typ-Sicherheit.

Bitwise: BitAnd, BitOr, BitXor, Shl, Shr, Not.

Für Flag-Sets, Bit-Manipulation, Kryptographie. Library-Patterns wie bitflags automatisieren die typische Boilerplate.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Traits

Zur Übersicht