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

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ą.

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

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