🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Przeciążanie operatorów w C# — podstawy

C# pozwala nadać własnym typom nowe znaczenie dla standardowych operatorów — +-==< i innych. To mechanizm, który sprawia, że DateTime.Now + TimeSpan.FromHours(1) albo vector1 + vector2 działa tak naturalnie, jak dodawanie dwóch liczb. W tym artykule zbudujemy przeciążanie operatorów od podstaw: poznasz pełną listę przeciążalnych operatorów, regułę par, oraz najczęstsze pułapki, które psują semantykę typu zamiast ją ułatwiać.

Jeśli szukasz bardziej praktycznego, biznesowego przykładu (klasa faktur, generic math, kiedy NIE przeciążać operatorów), zobacz część drugą: Przeciążanie operatorów w C# — część 2.

Czym jest przeciążanie operatorów

Przeciążanie operatorów (ang. operator overloading) to definiowanie statycznych funkcji składowych za pomocą słowa kluczowego operator, dzięki którym typ zdefiniowany przez programistę (klasa lub struktura) zyskuje obsługę operatorów wbudowanych w język.

Załóżmy, że mamy strukturę reprezentującą liczbę zespoloną:

public struct Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }

    public override string ToString() => $"{Real} + {Imaginary}i";
}

Bez dodatkowego kodu poniższe wyrażenie się nie skompiluje:

Complex a = new Complex { Real = 1, Imaginary = 2 };
Complex b = new Complex { Real = 4, Imaginary = 8 };
Complex c = a + b; // błąd kompilacji: Operator '+' cannot be applied

Aby to naprawić, przeciążamy operator dodawania za pomocą statycznej metody:

public struct Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }

    public static Complex operator +(Complex c1, Complex c2)
    {
        return new Complex
        {
            Real = c1.Real + c2.Real,
            Imaginary = c1.Imaginary + c2.Imaginary
        };
    }

    public override string ToString() => $"{Real} + {Imaginary}i";
}
Complex a = new Complex { Real = 1, Imaginary = 2 };
Complex b = new Complex { Real = 4, Imaginary = 8 };
Complex c = a + b;

Console.WriteLine(c); // 5 + 10i

Zasady, których nie da się obejść

  • Metoda przeciążająca operator musi być public i static.
  • Nie może być wirtualna, abstrakcyjna ani zamknięta (sealed) — bo jest statyczna, więc polimorfizm jej nie dotyczy.
  • Liczba argumentów jest ściśle określona przez rodzaj operatora — operator dwuargumentowy (np. +) przyjmuje dokładnie dwa parametry, operator jednoargumentowy (np. unarny -) przyjmuje jeden.
  • Przynajmniej jeden z parametrów musi być tego samego typu co klasa/struktura, w której operator jest zdefiniowany (nie można przeciążyć np. int + int).

Pełna lista przeciążalnych operatorów

  • Arytmetyczne: +-*/%
  • Unarne: +-!~++--
  • Bitowe: &|^<<>>
  • Porównania: ==!=<><=>=
  • Nie do przeciążenia: =, operatory logiczne &&/|| (wynikają pośrednio z &/| i true/false), operator warunkowy ?:, indeksator [] (ten definiuje się inaczej — przez this[int index]).

Pełną, aktualną listę wraz z ograniczeniami znajdziesz w oficjalnej dokumentacji: Microsoft Learn — Overloadable operators.

Reguła par — operatory, które muszą iść w komplecie

Kompilator wymusza przeciążanie niektórych operatorów parami:

  • jeśli przeciążysz ==, musisz też przeciążyć != (i odwrotnie),
  • jeśli przeciążysz <, musisz też przeciążyć >,
  • jeśli przeciążysz <=, musisz też przeciążyć >=.

Co ważne — operatory porównania takie jak == i != zwracają bool, czyli inny typ niż argumenty, którymi operują. To dozwolone: typ zwracany operatora nie musi odpowiadać typowi parametrów.

Pułapka: == bez Equals i GetHashCode

To najczęstszy błąd przy przeciążaniu operatorów porównania. Jeśli przeciążasz ==, powinieneś też nadpisać Equals(object) i GetHashCode() tak, by dawały spójny wynik — inaczej ten sam obiekt może być “równy” przez ==, a “nierówny” przez Equals (np. w Dictionary czy HashSet, które używają Equals/GetHashCode, a nie ==).

public struct Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }

    public static bool operator ==(Complex a, Complex b)
        => a.Real == b.Real && a.Imaginary == b.Imaginary;

    public static bool operator !=(Complex a, Complex b)
        => !(a == b);

    public override bool Equals(object? obj)
        => obj is Complex other && this == other;

    public override int GetHashCode()
        => HashCode.Combine(Real, Imaginary);
}

Compiler w Visual Studio/Rider zresztą sam ostrzeże (CS0660/CS0661), jeśli przeciążysz ==/!= bez nadpisania Equals/GetHashCode — warto to ostrzeżenie traktować poważnie, nie ignorować.

Operatory jednoargumentowe (unarne)

Operatory arytmetyczne, które poznaliśmy wyżej, są dwuargumentowe — przyjmują dwa operandy. C# pozwala też przeciążać operatory jednoargumentowe, takie jak unarny minus (negacja) czy ++/--. Rozszerzmy naszą strukturę Complex:

public struct Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }

    // Unarny minus — negacja liczby zespolonej
    public static Complex operator -(Complex c)
        => new Complex { Real = -c.Real, Imaginary = -c.Imaginary };

    // Inkrementacja — zwiększa obie składowe o 1
    public static Complex operator ++(Complex c)
        => new Complex { Real = c.Real + 1, Imaginary = c.Imaginary + 1 };
}
Complex a = new Complex { Real = 3, Imaginary = 5 };
Complex negated = -a;      // -3 + -5i
a++;                        // kompilator sam wywoła operator ++ i podstawi wynik

Zwróć uwagę, że kompilator sam odróżnia wersję jednoargumentową - od dwuargumentowej po liczbie parametrów w sygnaturze — możesz mieć obie zdefiniowane jednocześnie w tej samej klasie, bez konfliktu nazw.

Operatory checked (C# 11+)

Od C# 11 można zdefiniować osobną, “kontrolowaną” wersję operatora arytmetycznego za pomocą operator checked — uruchamianą, gdy kod wykonuje się w kontekście checked i ma reagować na przepełnienie inaczej niż wersja domyślna:

public static Complex operator +(Complex a, Complex b) => /* wersja unchecked */;
public static Complex operator checked +(Complex a, Complex b) => /* wersja checked, rzuca np. OverflowException */;

To głównie zastosowanie dla bibliotek numerycznych, gdzie przepełnienie ma inne konsekwencje niż w zwykłym kodzie biznesowym — ale warto wiedzieć, że taka możliwość istnieje.

Kiedy warto, a kiedy nie

Przeciążanie operatorów najlepiej sprawdza się dla typów, które matematycznie reprezentują wartość — liczby zespolone, wektory, pieniądze, ułamki, jednostki miary. Dla typów biznesowych bez naturalnej algebry (np. KlientZamówienie) przeciążony + zwykle myli czytelnika bardziej, niż pomaga — czytelna metoda nazwana (Zamówienie.Połącz(inne)) jest wtedy lepszym wyborem.

Kontynuacja tego tematu — generic math, operatory konwersji i LINQ — czeka w Przeciążanie operatorów w C#, część 2. Przeciążanie metod to też forma polimorfizmu — zobacz Polimorfizm w C#.

FAQ — przeciążanie operatorów w C#

Czy operator musi zwracać ten sam typ co argumenty?
Nie. Operatory porównania (==< itd.) typowo zwracają bool, a nie typ, na którym operują.

Czy mogę przeciążyć operator dla typu wbudowanego, np. int?
Nie — przynajmniej jeden z parametrów operatora musi być typem zdefiniowanym przez Ciebie.

Co się stanie, jeśli przeciążę == bez !=?
Kod się nie skompiluje — kompilator wymusza przeciążanie tych dwóch operatorów parami.

Czy += trzeba przeciążać osobno?
Nie w typowym przypadku — C# automatycznie wyprowadza zachowanie += z przeciążonego +. Więcej o tym w części 2.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

5 comments

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →