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.
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 Methodeself(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.
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
| Operator | Trait | Methode |
|---|---|---|
+ | Add | add |
- | Sub | sub |
* | Mul | mul |
/ | Div | div |
% | Rem | rem |
-x (unär) | Neg | neg |
+= | AddAssign | add_assign |
-= | SubAssign | sub_assign |
*= | MulAssign | mul_assign |
/= | DivAssign | div_assign |
== / != | PartialEq | eq / ne |
<, >, <=, >= | PartialOrd | lt, gt, le, ge |
&, |, ^ | BitAnd, BitOr, BitXor | bitand, bitor, bitxor |
<<, >> | Shl, Shr | shl, shr |
[index] | Index / IndexMut | index / index_mut |
*x (Dereferenzierung) | Deref / DerefMut | deref / deref_mut |
! | Not | not |
.., ..= | 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.
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.
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.
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.
# 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
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
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
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
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
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
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
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
- The Rust Book – Operator Overloading
- std::ops – Operator Traits
- std::cmp – Comparison Traits
- Rust Reference – Operator Expressions