Przeciążanie operatorów w C#, część 2

W 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 (int, double) 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 — 1230Róż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 + invoiceBma 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 + bnie powinno modyfikowaćaanib, 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.
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
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ę →
1 comment