1. Einführung
Du bist bestimmt daran gewöhnt, Operatoren wie + zum Addieren von Zahlen oder == zum Vergleichen zu benutzen. Diese Operatoren funktionieren super mit eingebauten Datentypen. Aber was, wenn du deine eigene Klasse baust, sagen wir mal Vector (Vektor), und möchtest, dass das Addieren von zwei Vektoren natürlich aussieht, zum Beispiel vector1 + vector2? Oder dass der Vergleich von zwei Money (Geld)-Objekten wie moneyAmount1 == moneyAmount2 funktioniert? Genau für solche Szenarien gibt es in C# die Operatorüberladung.
Mit Operatorüberladung kannst du festlegen, wie Standardoperatoren (wie +, -, *, /, ==, !=, >, <, ++, -- und viele andere) mit Instanzen deiner eigenen Klassen oder Strukturen funktionieren sollen. Das macht deinen Code intuitiver, lesbarer und ausdrucksstärker, weil du den gewohnten Syntax auch für deine eigenen Typen nutzen kannst.
Grundlagen der Operatorüberladung
Um einen Operator zu überladen, deklarierst du eine statische Methode in deiner Klasse oder Struktur mit dem Schlüsselwort operator, gefolgt vom Operator-Symbol, das du überladen willst.
- Statische Methode: Die Überladungsmethode für Operatoren muss immer public static sein.
- Name der Methode: Der Methodenname ist das Schlüsselwort operator, gefolgt vom Operator-Symbol (zum Beispiel operator +, operator ==).
- Parameter: Die Anzahl der Parameter hängt vom Operator-Typ ab (unär oder binär).
- Unäre Operatoren (zum Beispiel +, -, !, ++, --): nehmen einen Parameter vom Typ der Klasse/Struktur, in der sie definiert sind.
- Binäre Operatoren (zum Beispiel +, -, *, /, ==, !=): nehmen zwei Parameter, wobei mindestens einer vom Typ der Klasse/Struktur sein muss, in der sie definiert sind.
- Rückgabetyp: Der Typ, den die Operation zurückgibt.
2. Überladung von binären Operatoren (Binary Operators)
Binäre Operatoren nehmen zwei Operanden. Die gängigsten sind arithmetische Operatoren (+, -, *, /) und Vergleichsoperatoren (==, !=, >, <, >=, <=).
Beispiel: Überladung des Additionsoperators (+)
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) => (X, Y) = (x, y);
// Überladung des Operators +
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
public override string ToString() => $"({X}, {Y})";
}
// Verwendung:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // Hier wird der überladene Operator + aufgerufen
Console.WriteLine($"Summe der Punkte: {sumPoint}"); // Ausgabe: Summe der Punkte: (4, 6)
Hier haben wir festgelegt, dass das Addieren von zwei Point-Objekten ein neues Point-Objekt zurückgibt, dessen Koordinaten die Summen der jeweiligen Koordinaten der Ausgangspunkte sind.
Beispiel: Überladung des Multiplikationsoperators (*) mit einem Skalar (Zahl)
public struct Vector
{
public double X { get; set; }
public double Y { get; set; }
public Vector(double x, double y) => (X, Y) = (x, y);
// Überladung des Operators * (Vektor mal Zahl)
public static Vector operator *(Vector vec, double scalar)
{
return new Vector(vec.X * scalar, vec.Y * scalar);
}
// Überladung des Operators * (Zahl mal Vektor) - für Symmetrie
public static Vector operator *(double scalar, Vector vec)
{
return new Vector(vec.X * scalar, vec.Y * scalar);
}
public override string ToString() => $"<{X}, {Y}>";
}
// Verwendung:
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5; // Ruft Vector * double auf
Vector scaledVec2 = 5 * vec1; // Ruft double * Vector auf
Console.WriteLine($"Skalierter Vektor 1: {scaledVec1}"); // Ausgabe: Skalierter Vektor 1: <10, 15>
Console.WriteLine($"Skalierter Vektor 2: {scaledVec2}"); // Ausgabe: Skalierter Vektor 2: <10, 15>
Achte darauf, dass wir den Multiplikationsoperator zweimal überladen haben, damit er symmetrisch funktioniert: Vector * double und double * Vector.
Beispiel: Überladung der Vergleichsoperatoren (== und !=)
Beim Überladen der Operatoren == und != musst du einen wichtigen Vertrag beachten: Wenn du einen davon überlädst, musst du auch den anderen überladen. Außerdem wird dringend empfohlen, dass das Verhalten dieser Operatoren mit dem von Equals() und GetHashCode() übereinstimmt.
public class Money
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Unbedingt Equals und GetHashCode überschreiben, wenn == / != überladen werden
public override bool Equals(object? obj)
{
if (obj is not Money other) return false;
return Amount == other.Amount && Currency == other.Currency;
}
public override int GetHashCode() => HashCode.Combine(Amount, Currency);
// Überladung des Operators ==
public static bool operator ==(Money? m1, Money? m2)
{
// Null-Check für Referenztypen
if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
return m1.Equals(m2); // Nutzt unsere überschriebene Equals-Methode
}
// Überladung des Operators != (Pflicht bei Überladung von ==)
public static bool operator !=(Money? m1, Money? m2)
{
return !(m1 == m2);
}
}
// Verwendung:
Money cash1 = new Money(100, "USD");
Money cash2 = new Money(100, "USD");
Money cash3 = new Money(50, "USD");
Money cash4 = new Money(100, "EUR");
Console.WriteLine($"cash1 == cash2: {cash1 == cash2}"); // True
Console.WriteLine($"cash1 == cash3: {cash1 == cash3}"); // False
Console.WriteLine($"cash1 == cash4: {cash1 == cash4}"); // False
Für Werttypen (Strukturen) führen die Operatoren == und != standardmäßig einen bitweisen Vergleich durch, was meistens passt. Für Klassen (class) vergleicht == standardmäßig die Referenzen, daher ist das Überladen hier sehr nützlich, um einen Vergleich nach Wert zu ermöglichen.
3. Überladung von unären Operatoren (Unary Operators)
Unäre Operatoren arbeiten mit einem Operanden. Beispiele: + (unäres Plus), - (unäres Minus), ! (logisches NICHT), ~ (bitweises NICHT), ++ (Inkrement), -- (Dekrement).
Beispiel: Überladung des unären Minusoperators (-)
public struct Vector3D
{
public double X, Y, Z;
public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);
// Überladung des unären Operators -
public static Vector3D operator -(Vector3D vec)
{
return new Vector3D(-vec.X, -vec.Y, -vec.Z);
}
public override string ToString() => $"<{X}, {Y}, {Z}>";
}
// Verwendung:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // Ruft den überladenen unären Operator - auf
Console.WriteLine($"Originalvektor: {originalVec}"); // Ausgabe: Originalvektor: <1, -2, 3>
Console.WriteLine($"Invertierter Vektor: {invertedVec}"); // Ausgabe: Invertierter Vektor: <-1, 2, -3>
Beispiel: Überladung der Operatoren Inkrement (++) und Dekrement (--)
Diese Operatoren verändern den Operanden und geben den veränderten Wert zurück.
public struct Counter
{
public int Value { get; set; }
public Counter(int value) => Value = value;
// Überladung des Inkrementoperators ++
public static Counter operator ++(Counter c)
{
// Wichtig: Gib ein NEUES Objekt zurück, wenn die Struktur unveränderlich ist,
// oder ändere das aktuelle und gib es zurück, wenn es eine Klasse ist
return new Counter(c.Value + 1);
}
// Überladung des Dekrementoperators --
public static Counter operator --(Counter c)
{
return new Counter(c.Value - 1);
}
public override string ToString() => $"[Zaehler: {Value}]";
}
// Verwendung:
Counter myCounter = new Counter(5);
myCounter++; // Postfix-Inkrement
Console.WriteLine(myCounter); // Ausgabe: [Zaehler: 6]
++myCounter; // Präfix-Inkrement
Console.WriteLine(myCounter); // Ausgabe: [Zaehler: 7]
myCounter--;
Console.WriteLine(myCounter); // Ausgabe: [Zaehler: 6]
Die überladenen Operatoren ++ und -- sind immer präfix (erst ändern, dann zurückgeben). Der Compiler sorgt automatisch für das richtige Verhalten bei der Postfix-Form.
4. Überladung von Typumwandlungsoperatoren
Du kannst festlegen, wie Instanzen deines Typs explizit oder implizit in einen anderen Typ (und umgekehrt) umgewandelt werden können.
- implicit (implizite Umwandlung): Wird verwendet, wenn die Umwandlung immer sicher ist und keine Daten verloren gehen (zum Beispiel int zu long).
- explicit (explizite Umwandlung): Wird verwendet, wenn die Umwandlung zu Datenverlust oder Fehlern führen kann und explizit angegeben werden muss ((Type)obj).
Beispiel: Implizite Umwandlung von Score zu int
public struct Score
{
public int Points { get; set; }
public Score(int points) => Points = points;
// Implizite Umwandlung von Score zu int
public static implicit operator int(Score s)
{
return s.Points;
}
}
// Verwendung:
Score examScore = new Score(95);
int scoreValue = examScore; // Implizite Umwandlung
Console.WriteLine($"Punkte: {scoreValue}"); // Ausgabe: Punkte: 95
Beispiel: Explizite Umwandlung von Celsius zu Fahrenheit
public struct Celsius
{
public double Degrees { get; set; }
public Celsius(double degrees) => Degrees = degrees;
}
public struct Fahrenheit
{
public double Degrees { get; set; }
public Fahrenheit(double degrees) => Degrees = degrees;
// Explizite Umwandlung von Fahrenheit zu Celsius
public static explicit operator Celsius(Fahrenheit f)
{
return new Celsius((f.Degrees - 32) * 5 / 9);
}
// Explizite Umwandlung von Celsius zu Fahrenheit
public static explicit operator Fahrenheit(Celsius c)
{
return new Fahrenheit(c.Degrees * 9 / 5 + 32);
}
}
// Verwendung:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // Explizite Umwandlung
Console.WriteLine($"25°C = {f.Degrees}°F"); // Ausgabe: 25°C = 77°F
Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // Explizite Umwandlung
Console.WriteLine($"212°F = {c2.Degrees}°C"); // Ausgabe: 212°F = 100°C
5. Einschränkungen und Empfehlungen
Nicht alle Operatoren können überladen werden: Du kannst Operatoren wie &&, ||, ?., new, typeof, is, as, == (für string, weil der schon in der Klasse string überladen ist und spezielles Verhalten hat), () (Methodenaufruf), = (Zuweisung) und einige andere nicht überladen.
Symmetrie: Wenn du einen binären Operator überlädst, ist es oft sinnvoll, ihn auch für die umgekehrte Reihenfolge der Operanden zu überladen, falls das Sinn macht (wie im Beispiel mit Vector * double).
Verträge: Halte dich immer an die Verträge, besonders bei Vergleichsoperatoren (==, !=, >, <, >=, <=) und deren Zusammenhang mit Equals() und GetHashCode(). Wenn du die Verträge brichst, kann das zu unerwartetem und fehlerhaftem Verhalten führen, besonders in Collections.
Lesbarkeit und Intuitivität: Überlade Operatoren nur, wenn das den Code wirklich intuitiver und lesbarer macht. Wenn das Operatorverhalten nicht offensichtlich ist oder verwirren könnte, nutze lieber normale Methoden mit klaren Namen (zum Beispiel Add() statt +).
Veränderbare (Mutable) Objekte: Sei vorsichtig beim Überladen von Operatoren für veränderbare Typen. Zum Beispiel Vector v1 = v2 + v3; erstellt standardmäßig ein neues Objekt und verändert nicht v2. Wenn dein Operator das bestehende Objekt verändert, kann das verwirrend sein.
Strukturen vs Klassen: Operatorüberladung wird häufiger für Strukturen verwendet, die oft Werte repräsentieren (zum Beispiel Point, ComplexNumber), und für die mathematische Operationen oder Wertvergleiche natürlich sind.
GO TO FULL VERSION