🔥 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#, część 2

pierwszej części poznałeś składnię i zasady przeciążania operatorów na przykładzie liczb zespolonych. Tym razem przechodzimy do bardziej życiowego przykładu — klasy reprezentującej faktury, którą nauczymy się dodawać do siebie tak, jak dodaje się liczby. Zobaczysz też, co dzieje się z operatorem += po przeciążeniu +, jak przeciążanie operatorów łączy się z nowoczesnym generic math w .NET, i — co równie ważne — kiedy lepiej z tego nie korzystać.

Przykład: klasa Invoice, którą można dodawać

Wyobraź sobie, że chcesz zsumować kilka faktur i otrzymać jedną, zbiorczą wartość. Zaczynamy od prostej klasy:

public class Invoice
{
    public string Number { get; set; } = string.Empty;
    public decimal NetAmount { get; set; }
    public decimal VatAmount { get; set; }

    public decimal Total => NetAmount + VatAmount;
}

Mamy dwie przykładowe faktury:

var invoiceA = new Invoice { Number = "FV/1/2026", NetAmount = 1000m, VatAmount = 230m };
var invoiceB = new Invoice { Number = "FV/2/2026", NetAmount = 500m, VatAmount = 115m };

Próba dodania ich do siebie nie skompiluje się:

var suma = invoiceA + invoiceB; // błąd: Operator '+' cannot be applied to operands of type 'Invoice'

Naprawiamy to, dodając operator +, który zwraca nowy obiekt reprezentujący sumę kwot:

public class Invoice
{
    public string Number { get; set; } = string.Empty;
    public decimal NetAmount { get; set; }
    public decimal VatAmount { get; set; }

    public decimal Total => NetAmount + VatAmount;

    public static Invoice operator +(Invoice a, Invoice b)
    {
        return new Invoice
        {
            Number = $"{a.Number}+{b.Number}",
            NetAmount = a.NetAmount + b.NetAmount,
            VatAmount = a.VatAmount + b.VatAmount
        };
    }
}
var suma = invoiceA + invoiceB;

Console.WriteLine($"Suma netto: {suma.NetAmount} zł");
Console.WriteLine($"Suma VAT: {suma.VatAmount} zł");
Console.WriteLine($"Suma brutto: {suma.Total} zł");

// Suma netto: 1500 zł
// Suma VAT: 345 zł
// Suma brutto: 1845 zł

Co z operatorem += ?

Dobra wiadomość: nie musisz osobno przeciążać +=. C# automatycznie wyprowadza jego zachowanie z przeciążonego + — a += b kompilator tłumaczy na a = a + b. Dotyczy to każdego operatora złożonego (compound assignment): -=*=/= itd. działają analogicznie, o ile przeciążyłeś bazowy operator arytmetyczny.

var suma = invoiceA;
suma += invoiceB; // działa, bo += korzysta z przeciążonego operatora +

Sumowanie kolekcji faktur z LINQ

Skoro Invoice obsługuje teraz +, można naturalnie zsumować całą listę za pomocą Aggregate:

var invoices = new List<Invoice> { invoiceA, invoiceB, /* ... */ };

var total = invoices.Aggregate((a, b) => a + b);

Console.WriteLine($"Suma wszystkich faktur brutto: {total.Total} zł");

Przeciążanie operatorów a generic math (.NET 7+)

Od .NET 7 platforma wprowadziła interfejsy tzw. generic math (INumber<T>IAdditionOperators<TSelf, TOther, TResult> i pokrewne), które formalizują przeciążone operatory jako część kontraktu interfejsu. Dzięki temu można pisać metody generyczne działające na dowolnym typie liczbowym — wbudowanym (intdouble) i własnym (jak nasz Invoice), o ile zaimplementuje odpowiedni interfejs operatorowy:

public class Invoice : IAdditionOperators<Invoice, Invoice, Invoice>
{
    // ...pola jak wyżej...

    public static Invoice operator +(Invoice a, Invoice b) => /* jak wyżej */;
}

static T SumAll<T>(IEnumerable<T> items) where T : IAdditionOperators<T, T, T>
    => items.Aggregate((a, b) => a + b);

To pozwala napisać jedną funkcję SumAll, która działa zarówno na liczbach, jak i na własnych typach z przeciążonym + — bez przeciążania metody dla każdego typu osobno.

Operatory konwersji — implicit i explicit

Blisko spokrewniony z przeciążaniem operatorów jest mechanizm operatorów konwersji — pozwalają one automatycznie (lub na żądanie) zamienić Twój typ na inny. Wyobraźmy sobie, że chcemy móc odczytać kwotę brutto faktury po prostu rzutując obiekt na decimal:

public class Invoice
{
    public decimal NetAmount { get; set; }
    public decimal VatAmount { get; set; }
    public decimal Total => NetAmount + VatAmount;

    // Konwersja jawna — wymaga rzutowania (decimal)invoice
    public static explicit operator decimal(Invoice invoice) => invoice.Total;
}
var invoice = new Invoice { NetAmount = 1000m, VatAmount = 230m };
decimal amount = (decimal)invoice; // jawne rzutowanie — 1230

Różnica między implicit a explicit to nie tylko składnia, ale świadoma decyzja projektowa:

  • implicit — konwersja dzieje się automatycznie, bez rzutowania. Używaj tylko, gdy konwersja nigdy nie traci danych i nigdy nie rzuca wyjątku (np. int → double).
  • explicit — konwersja wymaga jawnego rzutowania (Typ)wartość. Używaj, gdy konwersja może stracić precyzję, rzucić wyjątkiem, albo po prostu nie jest oczywista dla czytelnika (jak w przykładzie z fakturą — zamiana obiektu biznesowego na “gołą” liczbę powinna być świadomą decyzją programisty, nie czymś, co dzieje się w tle).

Zasada praktyczna: w razie wątpliwości wybieraj explicit. Niejawne konwersje potrafią maskować błędy — kod kompiluje się i wygląda niewinnie, a semantycznie robi coś zaskakującego.

Kiedy NIE przeciążać operatorów

Przeciążanie operatorów bywa nadużywane. Zanim dodasz operator + do klasy biznesowej, zadaj sobie pytanie: czy to naprawdę wartość matematyczna, czy operacja biznesowa udająca matematykę?

  • Jeśli invoiceA + invoiceB ma dla czytelnika oczywiste, jednoznaczne znaczenie (suma kwot) — przeciążanie ma sens.
  • Jeśli znaczenie jest niejednoznaczne (np. co znaczy klientA + klientB?) — użyj metody nazwanej, np. Klient.Połącz(inny), zamiast zmuszać czytelnika do zgadywania.
  • Unikaj efektów ubocznych w operatorach — a + b nie powinno modyfikować a ani b, tylko zwracać nowy obiekt (jak w naszym przykładzie z fakturami).
  • Nie przeciążaj operatora tylko po to, by “zaoszczędzić” kilka znaków — czytelność kodu jest ważniejsza niż zwięzłość składni.

Jeśli nie widziałeś podstaw — zasad, pełnej listy przeciążalnych operatorów i pułapki == bez Equals — zacznij od Przeciążanie operatorów w C#. Generic math łączy się też z C# Records.

FAQ — przeciążanie operatorów w praktyce

Czy przeciążony operator + może rzucać wyjątek?
Tak, jeśli to uzasadnione (np. przy niekompatybilnych walutach w klasie Money) — ale powinno to być jasno udokumentowane, bo programiści nie spodziewają się wyjątków po zwykłym +.

Czy da się przeciążyć operator dla rekordów (record)?
Tak, dokładnie tak samo jak dla klasy czy struktury — rekordy same generują Equals/== na podstawie wartości pól, ale operatory arytmetyczne trzeba nadal zdefiniować ręcznie.

Czy Aggregate jest jedynym sposobem sumowania kolekcji własnego typu?
Nie — możesz też napisać zwykłą pętlę foreach z akumulatorem. Aggregate jest po prostu bardziej zwięzłym, funkcyjnym odpowiednikiem tego samego.

Czy interfejsy generic math (IAdditionOperators itd.) są obowiązkowe?
Nie — to opcjonalny mechanizm z .NET 7+, przydatny głównie przy pisaniu bibliotek generycznych działających na wielu typach liczbowych naraz. Zwykłe przeciążenie operator + działa samodzielnie, bez implementowania żadnego interfejsu.

👨‍💻
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.

1 comment

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ę →