Operatory w C# — arytmetyczne, logiczne, ??, ?. i ternary na przykładach
int wynik = 7 / 2;
Console.WriteLine(wynik); // 3, nie 3.5
Ten jeden przykład zaskakuje więcej początkujących niż cała reszta operatorów razem wzięta. Zanim przejdziemy do nowoczesnych ??, ?. i ?:, warto uporządkować podstawy — bo to właśnie tam czają się najczęstsze, ciche błędy.
Operatory arytmetyczne
int suma = 5 + 3; // 8
int roznica = 5 - 3; // 2
int iloczyn = 5 * 3; // 15
int iloraz = 7 / 2; // 3 — dzielenie całkowite OBCINA resztę
int reszta = 7 % 2; // 1 — operator modulo, reszta z dzielenia
Pułapka: dzielenie całkowite ucina część ułamkową
int a = 7, b = 2;
double wynikZly = a / b; // 3.0 — dzielenie już się wydarzyło jako int PRZED przypisaniem do double!
double wynikDobry = (double)a / b; // 3.5 — rzutowanie PRZED dzieleniem
Console.WriteLine(wynikZly); // 3
Console.WriteLine(wynikDobry); // 3.5
Kolejność ma tu znaczenie: a / b wykonuje się zanim wynik trafi do zmiennej double — jeśli oba operandy są typu int, dzielenie jest całkowite (ucina resztę), niezależnie od typu zmiennej, do której przypisujesz wynik. Żeby dostać dzielenie zmiennoprzecinkowe, rzutuj przynajmniej jeden z operandów na double przed dzieleniem, nie wynik po.
Operatory złożonego przypisania i inkrementacji
int licznik = 10;
licznik += 5; // to samo co: licznik = licznik + 5; → 15
licznik -= 3; // → 12
licznik *= 2; // → 24
int x = 5;
Console.WriteLine(x++); // 5 — WYPISUJE starą wartość, DOPIERO POTEM zwiększa (post-inkrementacja)
Console.WriteLine(x); // 6
int y = 5;
Console.WriteLine(++y); // 6 — zwiększa NAJPIERW, potem wypisuje (pre-inkrementacja)
Różnica między x++ (post-inkrementacja) a ++x (pre-inkrementacja) ujawnia się tylko wtedy, gdy wynik operacji od razu gdzieś wykorzystujesz (przypisanie, argument metody, wypisanie) — jako samodzielna instrukcja (x++; w osobnej linii) zachowują się identycznie. W wyrażeniu złożonym różnica jest realna i bywa źródłem błędów o jeden (off-by-one).
Operatory porównania i logiczne
bool rowne = (5 == 5); // true
bool rozne = (5 != 3); // true
bool i = (5 > 3) && (2 < 4); // true — oba warunki muszą być prawdziwe
bool lub = (5 > 10) || (2 < 4); // true — wystarczy jeden prawdziwy
bool zaprzeczenie = !false; // true
Pułapka: && i || są leniwe (short-circuit), & i | nie są
bool CzyDrogie(Produkt? produkt)
{
return produkt != null && produkt.Cena > 100; // ✅ bezpieczne — jeśli produkt == null, druga część NIE jest sprawdzana
}
bool CzyDrogieZle(Produkt? produkt)
{
return produkt != null & produkt.Cena > 100; // ❌ NullReferenceException, gdy produkt == null!
}
&& i || to operatory leniwe (short-circuit) — jeśli lewa strona && jest false, prawa strona w ogóle się nie wykonuje, bo wynik i tak będzie false. To nie tylko optymalizacja — to bezpieczeństwo: produkt != null && produkt.Cena > 100 nigdy nie odwoła się do produkt.Cena, jeśli produkt jest null. Pojedyncze & i | (bez podwojenia) zawsze sprawdzają obie strony — w powyższym przykładzie to prosta droga do NullReferenceException. W 99% przypadków chcesz &&/||, nie &/| (te drugie mają sens głównie przy operacjach bitowych na liczbach).
Null-coalescing ?? — wartość domyślna dla null
string? pseudonim = null;
string wyswietlana = pseudonim ?? "Anonim"; // "Anonim" — bo pseudonim jest null
string? imie = "Anna";
string wyswietlana2 = imie ?? "Anonim"; // "Anna" — bo imie NIE jest null
?? zwraca lewą stronę, jeśli nie jest null, w przeciwnym razie zwraca prawą. To krótszy zamiennik dla:
string wyswietlana = (pseudonim != null) ? pseudonim : "Anonim"; // to samo, dłużej
??= — przypisanie tylko gdy null
string? nazwa = null;
nazwa ??= "Wartość domyślna"; // przypisuje TYLKO jeśli nazwa jest null
Console.WriteLine(nazwa); // "Wartość domyślna"
nazwa ??= "Inna wartość"; // nie przypisuje — nazwa nie jest już null
Console.WriteLine(nazwa); // wciąż "Wartość domyślna"
Przydatne do leniwej inicjalizacji — ustawiasz wartość tylko raz, przy pierwszym użyciu, bez pisania if (x == null) x = ....
Null-conditional ?. — bezpieczny dostęp do właściwości
Zamowienie? zamowienie = ZnajdzZamowienie(id);
string? status = zamowienie?.Status; // jeśli zamowienie == null, status = null, bez wyjątku
int? liczbaPozycji = zamowienie?.Pozycje?.Count; // łańcuch — każdy '?.' sprawdza null osobno
?. sprawdza null przed każdym dostępem do właściwości/metody — jeśli obiekt po lewej jest null, całe wyrażenie zwraca null zamiast rzucić NullReferenceException. Można łączyć kilka ?. w łańcuch (zamowienie?.Pozycje?.Count) — dowolne ogniwo będące null bezpiecznie zatrzymuje resztę łańcucha.
Połączenie ?. z ?? to bardzo częsty, praktyczny wzorzec:
int liczbaPozycji = zamowienie?.Pozycje?.Count ?? 0; // 0, jeśli cokolwiek po drodze jest null
Operator warunkowy (ternary) ?:
int wiek = 17;
string kategoria = wiek >= 18 ? "Dorosły" : "Niepełnoletni";
warunek ? wartoscGdyPrawda : wartoscGdyFalsz to zwięzły zamiennik dla prostego if/else, które tylko przypisuje wartość. Dla logiki bardziej złożonej niż jedno przypisanie, zwykły if/else jest czytelniejszy — ternary zagnieżdżony w ternary (a ? b : c ? d : e) to prosta droga do nieczytelnego kodu.
Kolejność wykonywania (precedencja)
int wynik = 2 + 3 * 4; // 14, nie 20 — mnożenie przed dodawaniem
int wynik2 = (2 + 3) * 4; // 20 — nawiasy wymuszają kolejność
C# stosuje standardową matematyczną kolejność działań (mnożenie/dzielenie przed dodawaniem/odejmowaniem). Gdy masz wątpliwości albo wyrażenie miesza kilka typów operatorów (arytmetyczne, logiczne, porównania), dodaj nawiasy jawnie — nawet jeśli technicznie niepotrzebne, poprawiają czytelność i eliminują ryzyko pomyłki.
Co dalej
Ten artykuł pokazuje operatory wbudowane — jak używać +, ==, && na typach, które C# już zna. Jeśli chcesz zdefiniować własne znaczenie operatora + dla swojej klasy (np. dodawanie dwóch obiektów Pieniadze), to już inny temat — Przeciążanie operatorów w C#.
Podsumowanie
Dzielenie całkowite (int / int) ucina resztę niezależnie od typu zmiennej docelowej — rzutuj operand przed dzieleniem, nie wynik po. &&/|| są leniwe i bezpieczne przy sprawdzaniu null przed dostępem do właściwości; &/| sprawdzają zawsze obie strony i mogą rzucić wyjątek tam, gdzie &&/|| by go uniknęły. ??, ??= i ?. to zwięzłe, bezpieczne narzędzia do pracy z wartościami, które mogą być null — połączone razem (x?.y ?? domyslna) obsługują cały łańcuch niepewności jedną linijką.
🚀 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ę →