Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

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!

📅 Timeline - ewolucja operators
  • C# 1.0 (2002) - Operator overloading (+, -, *, /, ==, !=, etc.)
  • C# 1.0 (2002) - Conversion operators (implicit, explicit)
  • C# 7.0 (2017) - ref returns w operatorach
  • C# 11 (2022) - Generic math - operatory w generic constraints
  • C# 14 (2026) - 🔥 Compound assignment operators! (+=, -=, *=, /=)
Operator Overloading - klasyka

Podstawy - przeciążanie operatorów

🔍 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 +=, -=, etc!

🎉 NOWOŚĆ C# 14 - Compound Assignment Operators

Możesz definiować +=, -=, *=, /= BEZPOŚREDNIO! Optymalizuj 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!
Podsumowanie

  • Operator overloading - +, -, *, /, ==, !=, <, >, etc.
  • Zasady - public static, pary (== z !=, < z >)
  • 🔥 Compound operators (C# 14) - +=, -=, *=, /= in-place!
  • Performance - zero alokacji z compound operators
  • Implicit conversion - automatyczna, bezpieczna
  • Explicit conversion - wymaga cast, może stracić dane
  • Practical scenarios - Money, Vector3, Units

W kolejnym wpisie skupimy się na obsłudze błędów i wyjątków - try/catch/finally, wlasne wyjątki, globalna obsługa wyjątków.