User-defined operators to sposób na naturalną składnię dla własnych typów! W 2015 roku mogłeś przeciążać operatory arytmetyczne i porównania. W 2026 roku masz dodatkowo compound assignment operators (C# 14) - możesz definiować `+=`, `-=` bezpośrednio!
W tym wpisie poznasz operator overloading (klasyka), compound assignment operators (C# 14), conversion operators, i praktyczne scenariusze użycia!
🔍 Operator overloading = definiujesz jak operator działa dla twojego typu
Zamiast vector1.Add(vector2) możesz pisać vector1 + vector2
Naturalniejsza składnia, bardziej czytelny kod!
// Przykład: Vector2D
public struct Vector2D
{
public double X { get; }
public double Y { get; }
public Vector2D(double x, double y)
{
X = x;
Y = y;
}
// Operator + (dodawanie wektorów)
public static Vector2D operator +(Vector2D a, Vector2D b)
{
return new Vector2D(a.X + b.X, a.Y + b.Y);
}
// Operator - (odejmowanie wektorów)
public static Vector2D operator -(Vector2D a, Vector2D b)
{
return new Vector2D(a.X - b.X, a.Y - b.Y);
}
// Operator * (mnożenie przez skalar)
public static Vector2D operator *(Vector2D v, double scalar)
{
return new Vector2D(v.X * scalar, v.Y * scalar);
}
// Operator * (odwrotna kolejność)
public static Vector2D operator *(double scalar, Vector2D v)
{
return v * scalar; // Deleguj do poprzedniego
}
}
// Użycie - naturalna składnia! ✨
var v1 = new Vector2D(3, 4);
var v2 = new Vector2D(1, 2);
var sum = v1 + v2; // (4, 6)
var diff = v1 - v2; // (2, 2)
var scaled = v1 * 2; // (6, 8)
var scaled2 = 2 * v1; // (6, 8) - odwrotna kolejność!
Operatory arytmetyczne
// Kompleksowy przykład: Money type
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Operator + (dodawanie)
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Różne waluty!");
return new Money(a.Amount + b.Amount, a.Currency);
}
// Operator - (odejmowanie)
public static Money operator -(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Różne waluty!");
return new Money(a.Amount - b.Amount, a.Currency);
}
// Operator * (mnożenie przez liczba)
public static Money operator *(Money money, decimal multiplier)
{
return new Money(money.Amount * multiplier, money.Currency);
}
// Operator / (dzielenie przez liczba)
public static Money operator /(Money money, decimal divisor)
{
return new Money(money.Amount / divisor, money.Currency);
}
// Operator unary - (negacja)
public static Money operator -(Money money)
{
return new Money(-money.Amount, money.Currency);
}
}
// Użycie
var price = new Money(100, "PLN");
var tax = new Money(23, "PLN");
var total = price + tax; // 123 PLN
var discount = price * 0.9m; // 90 PLN
var perPerson = total / 4; // 30.75 PLN
var debt = -price; // -100 PLN
Operatory porównania
// Operatory porównania - zawsze w parach!
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
// Operator == (musi być z !=)
public static bool operator ==(Money a, Money b)
{
return a.Amount == b.Amount && a.Currency == b.Currency;
}
// Operator != (musi być z ==)
public static bool operator !=(Money a, Money b)
{
return !(a == b);
}
// Operator < (musi być z >)
public static bool operator <(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Różne waluty!");
return a.Amount < b.Amount;
}
// Operator > (musi być z <)
public static bool operator >(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Różne waluty!");
return a.Amount > b.Amount;
}
// Operator <= (musi być z >=)
public static bool operator <=(Money a, Money b)
{
return a < b || a == b;
}
// Operator >= (musi być z <=)
public static bool operator >=(Money a, Money b)
{
return a > b || a == b;
}
// Equals i GetHashCode - zalecane gdy przeciążasz ==
public override bool Equals(object obj)
{
return obj is Money money && this == money;
}
public override int GetHashCode()
{
return HashCode.Combine(Amount, Currency);
}
}
// Użycie
var price1 = new Money(100, "PLN");
var price2 = new Money(200, "PLN");
bool equal = price1 == price2; // false
bool less = price1 < price2; // true
bool greater = price1 > price2; // false
⚠️ Zasady operator overloading
✅ Operatory muszą być public static
✅ == i != zawsze razem (para)
✅ < i > zawsze razem (para)
✅ <= i >= zawsze razem (para)
✅ Gdy przeciążasz ==, override Equals() i GetHashCode()
❌ Nie można przeciążać: =, &&, ||, ??, ?., [], (), new
🔥 Compound Assignment Operators (C# 14)
Problem przed C# 14
// Przed C# 14 - compound assignment używa + i =
public struct Vector2D
{
public static Vector2D operator +(Vector2D a, Vector2D b)
{
return new Vector2D(a.X + b.X, a.Y + b.Y);
}
}
var v = new Vector2D(3, 4);
v += new Vector2D(1, 2); // Kompilator: v = v + new Vector2D(1, 2)
// Problem:
// - Tworzy NOWY Vector2D (alokacja)
// - Dla struct: kopiowanie całej struktury
// - Dla dużych struktur: performance hit
// - Nie możesz zoptymalizować += oddzielnie od +
// C# 14 - bezpośrednia definicja compound operators
public struct Vector2D
{
public double X { get; set; }
public double Y { get; set; }
// Operator + (klasyczny)
public static Vector2D operator +(Vector2D a, Vector2D b)
{
return new Vector2D { X = a.X + b.X, Y = a.Y + b.Y };
}
// Operator += (C# 14 - zoptymalizowany!)
public static void operator +=(ref Vector2D a, Vector2D b)
{
a.X += b.X; // Modyfikuj in-place - BEZ alokacji!
a.Y += b.Y;
}
// Operator -= (C# 14)
public static void operator -=(ref Vector2D a, Vector2D b)
{
a.X -= b.X;
a.Y -= b.Y;
}
// Operator *= (mnożenie przez skalar)
public static void operator *=(ref Vector2D v, double scalar)
{
v.X *= scalar;
v.Y *= scalar;
}
}
// Użycie
var v = new Vector2D { X = 3, Y = 4 };
v += new Vector2D { X = 1, Y = 2 }; // Używa operator += - BEZ alokacji! ✨
v *= 2; // Używa operator *= - in-place!
// Zalety:
// ✅ Zero alokacji (in-place modification)
// ✅ Lepszy performance dla dużych struktur
// ✅ Oddzielna optymalizacja dla += vs +
Praktyczne przykłady - compound operators
// Przykład 1: BigInteger-like type (duże liczby)
public struct BigNumber
{
private int[] _digits; // Reprezentacja jako array cyfr
// Operator + - tworzy nowy BigNumber (alokacja!)
public static BigNumber operator +(BigNumber a, BigNumber b)
{
// Złożona logika dodawania z carry, etc.
var result = new BigNumber();
result._digits = new int[Math.Max(a._digits.Length, b._digits.Length) + 1];
// ... dodawanie
return result;
}
// Operator += - optymalizacja! (C# 14)
public static void operator +=(ref BigNumber a, BigNumber b)
{
// Możesz modyfikować a._digits in-place!
// Reuse existing array jeśli wystarczająco duży
if (a._digits.Length < b._digits.Length + 1)
{
Array.Resize(ref a._digits, b._digits.Length + 1);
}
// ... dodawanie in-place - ZERO alokacji dla a! ✨
}
}
// Przykład 2: Matrix type (macierz)
public struct Matrix
{
private double[,] _data;
public static Matrix operator +(Matrix a, Matrix b)
{
// Tworzy NOWĄ macierz - alokacja array!
var result = new Matrix(a.Rows, a.Cols);
for (int i = 0; i < a.Rows; i++)
for (int j = 0; j < a.Cols; j++)
result._data[i, j] = a._data[i, j] + b._data[i, j];
return result;
}
public static void operator +=(ref Matrix a, Matrix b)
{
// Modyfikuj a._data in-place - ZERO alokacji!
for (int i = 0; i < a.Rows; i++)
for (int j = 0; j < a.Cols; j++)
a._data[i, j] += b._data[i, j];
}
}
// Użycie - performance difference!
var m1 = new Matrix(1000, 1000);
var m2 = new Matrix(1000, 1000);
// Przed C# 14:
m1 = m1 + m2; // Alokuje NOWĄ macierz 1000x1000 (8MB!)
// C# 14:
m1 += m2; // Modyfikuje in-place - ZERO alokacji! ✨
// W pętli - ogromna różnica!
for (int i = 0; i < 100; i++)
{
m1 = m1 + m2; // 100 * 8MB = 800MB alokacji! 😱
}
for (int i = 0; i < 100; i++)
{
m1 += m2; // ZERO alokacji! ✨
}
Compound operators - zasady
// Zasady compound operators (C# 14):
// 1. Sygnatura: ref dla pierwszego parametru
public static void operator +=(ref MyType a, MyType b) { } // ✅
// 2. Return type: void
public static void operator +=(ref MyType a, MyType b) { } // ✅
// public static MyType operator +=(ref MyType a, MyType b) { } // ❌
// 3. Możesz mieć + BEZ +=, lub += BEZ +
public static MyType operator +(MyType a, MyType b) { } // ✅ Samo +
public static void operator +=(ref MyType a, MyType b) { } // ✅ Samo +=
// 4. Kompilator używa += jeśli dostępny, inaczej fallback do +
var v = new Vector2D();
v += other; // Używa operator += jeśli zdefiniowany, inaczej v = v + other
// 5. Dostępne compound operators:
// +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, >>>=
Feature
Przed C# 14 (v = v + x)
C# 14 (v += x)
Alokacje
Tworzy nowy obiekt
In-place, zero alokacji
Performance
Wolniejszy
Szybszy
Kontrola
Używa +, brak kontroli
Pełna kontrola
Use case
Małe typy
Duże struktury, performance-critical
Conversion Operators - konwersje typów
Implicit conversion - automatyczna konwersja
// Implicit conversion - kompilator konwertuje automatycznie
public readonly struct Meters
{
public double Value { get; }
public Meters(double value) => Value = value;
// Implicit conversion: Meters → double
public static implicit operator double(Meters meters)
{
return meters.Value;
}
// Implicit conversion: double → Meters
public static implicit operator Meters(double value)
{
return new Meters(value);
}
}
// Użycie - automatyczna konwersja!
Meters distance = 100.5; // double → Meters (implicit)
double value = distance; // Meters → double (implicit)
void PrintDistance(double d) => Console.WriteLine(d);
PrintDistance(distance); // Meters → double (automatic!)
// Implicit = używaj gdy konwersja jest ZAWSZE bezpieczna
Explicit conversion - wymuszona konwersja
// Explicit conversion - wymaga rzutowania
public readonly struct Celsius
{
public double Value { get; }
public Celsius(double value) => Value = value;
}
public readonly struct Fahrenheit
{
public double Value { get; }
public Fahrenheit(double value) => Value = value;
// Explicit conversion: Celsius → Fahrenheit
public static explicit operator Fahrenheit(Celsius celsius)
{
return new Fahrenheit(celsius.Value * 9 / 5 + 32);
}
// Explicit conversion: Fahrenheit → Celsius
public static explicit operator Celsius(Fahrenheit fahrenheit)
{
return new Celsius((fahrenheit.Value - 32) * 5 / 9);
}
}
// Użycie - wymaga rzutowania (explicit cast)
var c = new Celsius(100);
var f = (Fahrenheit)c; // Explicit cast required!
// var f2 = c; // ❌ BŁĄD - nie działa bez rzutowania
// Explicit = używaj gdy konwersja może być nieoczywista lub stratna
Implicit vs Explicit - kiedy czego używać?
// ✅ Implicit - używaj gdy:
// - Konwersja jest zawsze bezpieczna (no data loss)
// - Konwersja jest oczywista
// - Nie ma ryzyka błędów
public struct SafeConversion
{
// int → long - zawsze bezpieczne (int mieści się w long)
public static implicit operator long(int value) => value;
// Meters → Centimeters - tylko zmiana jednostki
public static implicit operator Centimeters(Meters m)
=> new Centimeters(m.Value * 100);
}
// ❌ Explicit - używaj gdy:
// - Konwersja może stracić dane (data loss)
// - Konwersja może rzucić exception
// - Konwersja nie jest oczywista
public struct UnsafeConversion
{
// long → int - może stracić dane!
public static explicit operator int(long value)
=> checked((int)value); // Może throw OverflowException
// string → int - może fail!
public static explicit operator int(string s)
=> int.Parse(s); // Może throw FormatException
// Celsius → Fahrenheit - wymaga obliczeń, nie oczywiste
public static explicit operator Fahrenheit(Celsius c)
=> new Fahrenheit(c.Value * 9 / 5 + 32);
}
Praktyczne przykłady - conversions
// Przykład 1: Units of measurement
public readonly struct Kilometers
{
public double Value { get; }
public Kilometers(double value) => Value = value;
// Implicit ← Meters (większa jednostka ← mniejsza, bezpieczne)
public static implicit operator Kilometers(Meters meters)
=> new Kilometers(meters.Value / 1000);
}
public readonly struct Meters
{
public double Value { get; }
public Meters(double value) => Value = value;
// Explicit → Kilometers (mniejsza → większa, może stracić precyzję)
public static explicit operator Meters(Kilometers km)
=> new Meters(km.Value * 1000);
}
// Użycie
Meters m = new Meters(1500);
Kilometers km = m; // Implicit - OK
Meters m2 = (Meters)km; // Explicit - wymaga cast
// Przykład 2: Wrapper types
public readonly struct UserId
{
public int Value { get; }
public UserId(int value) => Value = value;
// Explicit conversion - nie chcemy przypadkowych konwersji!
public static explicit operator int(UserId userId) => userId.Value;
public static explicit operator UserId(int value) => new UserId(value);
}
// Użycie - wymusza intencję
var userId = new UserId(123);
int id = (int)userId; // Explicit - musi być intencjonalne!
// Zapobiega błędom typu:
void ProcessUser(int id) { }
void ProcessOrder(int id) { }
// ProcessUser(userId); // ❌ BŁĄD - UserId nie konwertuje implicit do int
ProcessUser((int)userId); // ✅ OK - explicit, intencjonalne
Practical Scenarios - praktyczne użycie
Scenariusz 1: System zarządzania finansami
// Real-world: System zarządzania finansami
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Arithmetic operators
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException($"Nie można dodać {a.Currency} i {b.Currency}");
return new Money(a.Amount + b.Amount, a.Currency);
}
public static Money operator -(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException($"Nie można odjąć {a.Currency} i {b.Currency}");
return new Money(a.Amount - b.Amount, a.Currency);
}
public static Money operator *(Money money, decimal multiplier)
=> new Money(money.Amount * multiplier, money.Currency);
public static Money operator /(Money money, decimal divisor)
=> new Money(money.Amount / divisor, money.Currency);
// Comparison operators
public static bool operator ==(Money a, Money b)
=> a.Amount == b.Amount && a.Currency == b.Currency;
public static bool operator !=(Money a, Money b) => !(a == b);
public static bool operator <(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Różne waluty");
return a.Amount < b.Amount;
}
public static bool operator >(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("Różne waluty");
return a.Amount > b.Amount;
}
public static bool operator <=(Money a, Money b) => a < b || a == b;
public static bool operator >=(Money a, Money b) => a > b || a == b;
public override bool Equals(object obj) => obj is Money m && this == m;
public override int GetHashCode() => HashCode.Combine(Amount, Currency);
public override string ToString() => $"{Amount:N2} {Currency}";
}
// Użycie - naturalna składnia!
var price = new Money(99.99m, "PLN");
var tax = new Money(22.99m, "PLN");
var discount = new Money(10m, "PLN");
var total = price + tax - discount; // 112.98 PLN
var perPerson = total / 4; // 28.25 PLN
var doubled = price * 2; // 199.98 PLN
if (total > new Money(100, "PLN"))
{
Console.WriteLine("Darmowa dostawa!");
}
Scenariusz 2: Programowanie gier 3D
// Real-world: Game engine - Vector3
public struct Vector3
{
public float X, Y, Z;
public Vector3(float x, float y, float z)
{
X = x; Y = y; Z = z;
}
// Arithmetic
public static Vector3 operator +(Vector3 a, Vector3 b)
=> new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vector3 operator -(Vector3 a, Vector3 b)
=> new Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
public static Vector3 operator *(Vector3 v, float s)
=> new Vector3(v.X * s, v.Y * s, v.Z * s);
public static Vector3 operator /(Vector3 v, float s)
=> new Vector3(v.X / s, v.Y / s, v.Z / s);
// Unary
public static Vector3 operator -(Vector3 v)
=> new Vector3(-v.X, -v.Y, -v.Z);
// C# 14 - Compound operators dla performance!
public static void operator +=(ref Vector3 a, Vector3 b)
{
a.X += b.X;
a.Y += b.Y;
a.Z += b.Z;
}
public static void operator *=(ref Vector3 v, float s)
{
v.X *= s;
v.Y *= s;
v.Z *= s;
}
// Helper methods
public float Length() => MathF.Sqrt(X * X + Y * Y + Z * Z);
public Vector3 Normalized() => this / Length();
public static float Dot(Vector3 a, Vector3 b)
=> a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
// Użycie w game loop
var position = new Vector3(0, 0, 0);
var velocity = new Vector3(1, 0.5f, 0);
var gravity = new Vector3(0, -9.8f, 0);
// Game loop - 60 FPS
float deltaTime = 1f / 60f;
for (int frame = 0; frame < 3600; frame++) // 1 minuta
{
velocity += gravity * deltaTime; // Grawitacja
position += velocity * deltaTime; // Ruch
// Dzięki operatorom - czytelny physics code! ✨
}
Scenariusz 3: Biblioteka do jednostek miary
// Real-world: Library do jednostek miary
public readonly struct Length
{
private readonly double _meters;
private Length(double meters) => _meters = meters;
// Factory methods
public static Length FromMeters(double m) => new Length(m);
public static Length FromKilometers(double km) => new Length(km * 1000);
public static Length FromCentimeters(double cm) => new Length(cm / 100);
// Properties
public double Meters => _meters;
public double Kilometers => _meters / 1000;
public double Centimeters => _meters * 100;
// Operators
public static Length operator +(Length a, Length b)
=> new Length(a._meters + b._meters);
public static Length operator -(Length a, Length b)
=> new Length(a._meters - b._meters);
public static Length operator *(Length length, double multiplier)
=> new Length(length._meters * multiplier);
public static Length operator /(Length length, double divisor)
=> new Length(length._meters / divisor);
// Comparison
public static bool operator <(Length a, Length b) => a._meters < b._meters;
public static bool operator >(Length a, Length b) => a._meters > b._meters;
public static bool operator <=(Length a, Length b) => a._meters <= b._meters;
public static bool operator >=(Length a, Length b) => a._meters >= b._meters;
public static bool operator ==(Length a, Length b) => a._meters == b._meters;
public static bool operator !=(Length a, Length b) => a._meters != b._meters;
public override bool Equals(object obj) => obj is Length l && this == l;
public override int GetHashCode() => _meters.GetHashCode();
}
// Użycie - type-safe calculations!
var marathon = Length.FromKilometers(42.195);
var halfMarathon = marathon / 2;
var tenK = Length.FromKilometers(10);
var total = marathon + tenK; // 52.195 km
if (total > Length.FromMeters(50000))
{
Console.WriteLine($"Dystans przekroczył 50km: {total.Kilometers:F2} km");
}
// Type safety - nie możesz przez przypadek dodać metrów do czasu!
// var invalid = marathon + TimeSpan.FromHours(1); // ❌ BŁĄD kompilacji!