Operatory porównania, logiczne i bitowe
Zanim program podejmie jakąkolwiek decyzję, musi coś porównać. Ta lekcja pokazuje, jak zadawać komputerowi pytania typu „czy większe”, „czy równe” i jak łączyć takie pytania w bardziej złożone warunki. Na końcu zaglądamy pod maskę — do operacji na pojedynczych bitach.
Czego się dziś nauczysz
- Zbudujesz wyrażenie logiczne z operatorów porównania
- Porównasz dwa napisy z uwzględnieniem wielkości liter i bez niej
- Połączysz kilka warunków operatorami
&&,||i! - Wyjaśnisz, czym jest ocena skrócona i jak chroni przed błędem
- Uprościsz zaprzeczony warunek prawami De Morgana
- Odczytasz zapis dwójkowy liczby i wykonasz na niej operację bitową
Operatory porównania
Każdy z nich bierze dwie wartości i zwraca true albo false — czyli wartość typu bool.
| Operator | Znaczenie | Przykład | Wynik |
|---|---|---|---|
== | równe | 5 == 5 | true |
!= | różne | 5 != 3 | true |
> | większe | 5 > 3 | true |
< | mniejsze | 5 < 3 | false |
>= | większe lub równe | 5 >= 5 | true |
<= | mniejsze lub równe | 3 <= 5 | true |
int wiek = 17;
bool czyPelnoletni = wiek >= 18; // false
bool czyNastolatek = wiek >= 13; // true
Console.WriteLine(czyPelnoletni); // False
Console.WriteLine(wiek != 17); // False
Console.WriteLine(wiek * 2 > 30); // True — najpierw mnożenie, potem porównanie
Jeden znak równości to co innego niż dwa
= przypisuje wartość. == porównuje. To dwie zupełnie różne operacje i mylenie ich jest klasycznym błędem początkującego.
Dobra wiadomość: w C# ten błąd zwykle nie przejdzie przez kompilator. Zapis if (x = 5) zgłosi błąd, bo przypisanie zwraca liczbę, a warunek wymaga wartości bool. W językach C i C++ taki kod kompiluje się bez słowa i psuje program.
Porównania działają też na znakach
Typ char jest porównywany według numeru znaku w tablicy Unicode. Dlatego 'a' < 'b' daje true, a 'Z' < 'a' również — wielkie litery mają w tej tablicy niższe numery niż małe.
Porównywanie tekstu
Operator == na napisach porównuje zawartość, a nie miejsce w pamięci. To wygodne i działa dokładnie tak, jak się spodziewasz.
string a = "Anna";
string b = "Anna";
string c = "ANNA";
Console.WriteLine(a == b); // True — ta sama treść
Console.WriteLine(a == c); // False — inna wielkość liter!
Wielkość liter ma znaczenie
Dla komputera "Anna" i "ANNA" to dwa różne napisy. Przy porównywaniu danych wpisanych przez użytkownika prawie zawsze chcesz to zignorować — inaczej ktoś, kto wpisze TAK zamiast tak, dostanie zły wynik.
Porównanie ignorujące wielkość liter
string odpowiedz = "TAK";
// Sposób 1 — sprowadzamy oba napisy do małych liter
bool zgoda1 = odpowiedz.ToLower() == "tak";
// Sposób 2 — mówimy wprost, że wielkość liter nas nie interesuje (zalecany)
bool zgoda2 = string.Equals(odpowiedz, "tak", StringComparison.OrdinalIgnoreCase);
Console.WriteLine(zgoda1); // True
Console.WriteLine(zgoda2); // True
| Sposób | Kiedy używać |
|---|---|
.ToLower() == |
Krótszy i czytelniejszy dla początkującego. Zupełnie wystarczy do zadań szkolnych i na egzaminie. |
string.Equals(..., OrdinalIgnoreCase) |
Poprawniejszy: nie tworzy nowego napisu w pamięci i nie zależy od ustawień regionalnych. W niektórych językach — na przykład tureckim — zamiana wielkości liter działa inaczej niż byśmy się spodziewali, a ten zapis jest na to odporny. |
Napisy porównujemy tylko na równość
Operatory < i > nie działają na napisach — kompilator zgłosi błąd. Żeby ustalić, który napis jest wcześniej w kolejności alfabetycznej, używa się metody string.Compare(a, b): zwraca liczbę ujemną, zero albo dodatnią. Wrócimy do niej w bloku o tekstach.
Porównywanie liczb z przecinkiem
Wracamy do problemu z lekcji 06. Typ double zapisuje ułamki w przybliżeniu, więc porównanie na równość bywa zawodne.
double a = 0.1 + 0.2;
double b = 0.3;
Console.WriteLine(a == b); // False ✗
double tolerancja = 0.000001;
Console.WriteLine(Math.Abs(a - b) < tolerancja); // True ✓
| Typ danych | Jak porównywać na równość |
|---|---|
int, long, char, bool | == — bez zastrzeżeń |
decimal | == — bez zastrzeżeń, bo liczy dziesiętnie |
double, float | przez tolerancję: Math.Abs(a - b) < 0.000001 |
string | == albo string.Equals z OrdinalIgnoreCase |
Operatory <, >, <= i >= na typie double działają normalnie — problem dotyczy tylko sprawdzania dokładnej równości.
Operatory logiczne — łączenie warunków
Rzadko wystarcza jeden warunek. Zwykle pytamy o kilka rzeczy naraz: „czy jest pełnoletni i ma prawo jazdy”.
| Operator | Nazwa | Zwraca true, gdy… |
|---|---|---|
&& | koniunkcja (I) | oba warunki są prawdziwe |
|| | alternatywa (LUB) | przynajmniej jeden warunek jest prawdziwy |
! | negacja (NIE) | warunek jest fałszywy — odwraca wartość |
Tabele prawdy
| A | B | A && B | A || B | !A |
|---|---|---|---|---|
| true | true | true | true | false |
| true | false | false | true | false |
| false | true | false | true | true |
| false | false | false | false | true |
Jak to zapamiętać bez tabeli
&& jest wymagające — wystarczy jeden fałsz, żeby całość była fałszywa.
|| jest wyrozumiałe — wystarczy jedna prawda, żeby całość była prawdziwa.
int wiek = 20;
bool maPrawoJazdy = true;
bool maSamochod = false;
// Wszystkie warunki muszą być spełnione
bool mozeProwadzic = wiek >= 18 && maPrawoJazdy;
Console.WriteLine(mozeProwadzic); // True
// Wystarczy jeden
bool maPojazd = maSamochod || wiek < 18;
Console.WriteLine(maPojazd); // False
// Negacja
Console.WriteLine(!maSamochod); // True
// Warunek złożony — przedział
int temperatura = 22;
bool przyjemnie = temperatura >= 18 && temperatura <= 25;
Console.WriteLine(przyjemnie); // True
Przedziału nie zapiszesz tak jak w matematyce
Matematyczny zapis 18 <= t <= 25 nie działa w C#. Kompilator policzyłby najpierw 18 <= t, dostał wartość bool, a potem próbował porównać ją z liczbą 25 — i zgłosił błąd.
Przedział zapisujemy zawsze jako dwa osobne warunki połączone &&: t >= 18 && t <= 25.
Ocena skrócona
C# nie sprawdza warunków dłużej, niż to konieczne. Gdy wynik jest już przesądzony, reszta wyrażenia w ogóle się nie wykonuje.
&& przerywa na pierwszym fałszu
Skoro jeden warunek jest fałszywy, całość na pewno będzie fałszywa. Po co sprawdzać dalej?
|| przerywa na pierwszej prawdzie
Skoro jeden warunek jest prawdziwy, całość na pewno będzie prawdziwa.
To nie jest tylko optymalizacja — to mechanizm, który pozwala pisać bezpieczny kod:
int dzielnik = 0;
int liczba = 100;
// ✓ Bezpiecznie — gdy dzielnik jest zerem, druga część się NIE wykona
bool wynik = dzielnik != 0 && liczba / dzielnik > 10;
Console.WriteLine(wynik); // False, bez żadnego błędu
// ✗ Odwrotna kolejność przerwie program
// bool zle = liczba / dzielnik > 10 && dzielnik != 0;
// └── DivideByZeroException, zanim dojdziemy do sprawdzenia
Zasada praktyczna
Warunek zabezpieczający stawiaj zawsze jako pierwszy. „Czy dzielnik nie jest zerem” przed dzieleniem, „czy tekst nie jest pusty” przed sięganiem po znak. Kolejność warunków w C# ma znaczenie i można ją świadomie wykorzystać.
Istnieją też operatory bez skracania
Pojedyncze & i | na wartościach bool działają podobnie, ale zawsze sprawdzają obie strony. W praktyce prawie nigdy się ich tak nie używa — ich głównym zastosowaniem są operacje bitowe, o których za chwilę. Zapamiętaj: przy warunkach zawsze podwójne && i ||.
Kolejność działań i prawa De Morgana
| Kolejność | Operatory | Uwaga |
|---|---|---|
| 1 | ! | negacja wykonuje się najwcześniej |
| 2 | * / % + - | arytmetyka przed porównaniami |
| 3 | < > <= >= | — |
| 4 | == != | równość po pozostałych porównaniach |
| 5 | && | koniunkcja przed alternatywą |
| 6 | || | — |
bool a = true, b = false, c = true;
// && wiąże mocniej niż ||, więc to wyrażenie...
bool wynik = a || b && c;
// ...kompilator czyta tak:
bool toSamo = a || (b && c); // true
Console.WriteLine(wynik == toSamo); // True
Nawiasy przy mieszaniu && i ||
Gdy w jednym warunku pojawiają się oba operatory, zawsze stawiaj nawiasy — nawet jeśli akurat nie są konieczne. Wyrażenie (wiek >= 18 && maPrawoJazdy) || jestInstruktorem czyta się od razu, a bez nawiasów trzeba się zatrzymać i przypomnieć sobie priorytety.
Prawa De Morgana
Dwie reguły, które pozwalają wnieść zaprzeczenie do środka nawiasu. Przy okazji zamieniają && na || i odwrotnie.
// Prawo pierwsze: !(A && B) == !A || !B
!(pada && zimno) ⟷ !pada || !zimno
// Prawo drugie: !(A || B) == !A && !B
!(weekend || swieto) ⟷ !weekend && !swieto
Po polsku brzmi to zupełnie naturalnie:
| Zapis zaprzeczony | To samo bez nawiasu |
|---|---|
| Nieprawda, że pada i jest zimno | Albo nie pada, albo nie jest zimno |
| Nieprawda, że weekend lub święto | Nie weekend i nie święto |
Po co to komu
Przydaje się, gdy chcesz odwrócić warunek, żeby stał się czytelniejszy. Zamiast if (!(wiek >= 18 && maZgode)) można napisać if (wiek < 18 || !maZgode) — to samo znaczenie, a od razu widać, w jakich sytuacjach warunek jest spełniony.
Operatory bitowe
Do tej pory traktowaliśmy liczbę jako całość. Operatory bitowe pozwalają pracować na pojedynczych bitach jej zapisu dwójkowego.
Przypomnienie: liczba 12 zapisana dwójkowo to 1100, bo 12 = 8 + 4. Liczba 10 to 1010, bo 10 = 8 + 2.
| Operator | Nazwa | Działanie na każdej parze bitów |
|---|---|---|
& | iloczyn bitowy (AND) | 1 tylko wtedy, gdy oba bity są 1 |
| | suma bitowa (OR) | 1, gdy przynajmniej jeden bit jest 1 |
^ | różnica symetryczna (XOR) | 1, gdy bity są różne |
~ | negacja bitowa (NOT) | odwraca każdy bit — jednoargumentowy |
<< | przesunięcie w lewo | przesuwa bity w lewo, dopisując zera |
>> | przesunięcie w prawo | przesuwa bity w prawo |
int a = 12; // 1100
int b = 10; // 1010
Console.WriteLine(a & b); // 8 1000 — jedynka tylko na pozycji, gdzie obie miały 1
Console.WriteLine(a | b); // 14 1110 — jedynka tam, gdzie choć jedna miała 1
Console.WriteLine(a ^ b); // 6 0110 — jedynka tam, gdzie bity się różnią
// Podgląd zapisu dwójkowego
Console.WriteLine(Convert.ToString(a, 2)); // 1100
Console.WriteLine(Convert.ToString(a & b, 2)); // 1000
Przesunięcia bitowe
int x = 5; // 101
Console.WriteLine(x << 1); // 10 1010 przesunięcie w lewo o 1 = mnożenie przez 2
Console.WriteLine(x << 3); // 40 101000 przesunięcie o 3 = mnożenie przez 8
Console.WriteLine(x >> 1); // 2 10 przesunięcie w prawo = dzielenie całkowite przez 2
Przesunięcie to mnożenie i dzielenie przez potęgi dwójki
Przesunięcie w lewo o n pozycji mnoży liczbę przez 2n. Przesunięcie w prawo — dzieli całkowicie. Kiedyś była to popularna sztuczka optymalizacyjna, bo procesor wykonywał ją szybciej niż mnożenie.
Dziś tego nie robimy. Współczesne kompilatory same zamieniają x * 8 na przesunięcie, gdy to się opłaca, a zapis x * 8 jest dużo czytelniejszy. Operatory bitowe stosuje się tam, gdzie naprawdę chodzi o bity: przy obsłudze sprzętu, formatach plików, maskach uprawnień i kompresji.
Typowe zastosowanie: maska bitowa
Jednym bajtem można zapisać osiem niezależnych informacji typu tak/nie. Każdy bit to jedna flaga.
// Każde uprawnienie to inny bit
const int Odczyt = 1; // 001
const int Zapis = 2; // 010
const int Usuwanie = 4; // 100
// Nadajemy dwa uprawnienia naraz — suma bitowa
int uprawnienia = Odczyt | Zapis; // 011 = 3
Console.WriteLine(uprawnienia); // 3
// Sprawdzamy, czy dane uprawnienie jest włączone — iloczyn bitowy
bool mozeCzytac = (uprawnienia & Odczyt) != 0;
bool mozeUsuwac = (uprawnienia & Usuwanie) != 0;
Console.WriteLine(mozeCzytac); // True
Console.WriteLine(mozeUsuwac); // False
// Dodajemy uprawnienie
uprawnienia = uprawnienia | Usuwanie; // 111 = 7
Console.WriteLine(uprawnienia); // 7
Nie myl & z &&
To dwa różne operatory. && łączy warunki logiczne i skraca ocenę. & działa na bitach liczb, a dla bool oblicza obie strony bez oceny skróconej. Jeden znak różnicy, a zupełnie inne znaczenie — dlatego przy warunkach zawsze pisz podwójny.
Zapis na warsztaty robotyki
Program sprawdza fikcyjne zasady organizatora warsztatów, przyjęte wyłącznie do ćwiczenia operatorów logicznych: ukończone 17 lat i 9 miesięcy, ukończone szkolenie wstępne oraz albo zgoda opiekuna (dla niepełnoletnich), albo ukończone 18 lat.
// Dane kandydata
int wiekMiesiace = 213; // 17 lat i 9 miesięcy
bool maSzkolenie = true;
bool maZgodeOpiekuna = true;
const int MinimalnyWiekMiesiace = 213; // 17 lat i 9 miesiecy
const int PelnoletnoscMiesiace = 216; // 18 lat
// Warunki cząstkowe — każdy nazwany, więc kod tłumaczy się sam
bool wiekOk = wiekMiesiace >= MinimalnyWiekMiesiace;
bool czyPelnoletni = wiekMiesiace >= PelnoletnoscMiesiace;
bool zgodaOk = czyPelnoletni || maZgodeOpiekuna;
// Warunek końcowy
bool mozeSieZapisac = wiekOk && maSzkolenie && zgodaOk;
Console.WriteLine($"Wiek w miesiacach: {wiekMiesiace} ({wiekMiesiace / 12} lat {wiekMiesiace % 12} mies.)");
Console.WriteLine($"Wiek wystarczajacy: {wiekOk}");
Console.WriteLine($"Pelnoletni: {czyPelnoletni}");
Console.WriteLine($"Szkolenie: {maSzkolenie}");
Console.WriteLine($"Zgoda w porzadku: {zgodaOk}");
Console.WriteLine();
Console.WriteLine($"MOZE SIE ZAPISAC: {mozeSieZapisac}");
Wiek w miesiacach: 213 (17 lat 9 mies.)
Wiek wystarczajacy: True
Pelnoletni: False
Szkolenie: True
Zgoda w porzadku: True
MOZE SIE ZAPISAC: True
Omówienie
| Fragment | Dlaczego tak |
|---|---|
| Wiek liczony w miesiącach | Warunek „17 lat i 9 miesięcy” nie da się sensownie zapisać na latach. Sprowadzenie wszystkiego do jednej jednostki upraszcza całą resztę — to częsty zabieg przy datach i czasie. |
| Rozbicie na warunki cząstkowe | Można było napisać jedno wielkie wyrażenie. Byłoby krótsze, ale nieczytelne i trudne do sprawdzenia. Nazwane warunki pośrednie są kluczową techniką — pozwalają też wypisać, który dokładnie warunek zawiódł. |
czyPelnoletni || maZgodeOpiekuna |
Alternatywa, bo wystarczy jedno z dwojga. Pełnoletni nie potrzebuje zgody; niepełnoletni musi ją mieć. |
wiekOk && maSzkolenie && zgodaOk |
Koniunkcja, bo tu wszystkie trzy warunki są obowiązkowe. Zwróć uwagę, że jeden z nich sam w sobie jest alternatywą — warunki można zagnieżdżać dowolnie. |
{wiekMiesiace / 12} i {wiekMiesiace % 12} |
Znajomy duet z poprzedniej lekcji: dzielenie daje pełne lata, reszta — miesiące. |
Spróbuj sam
Zmień maSzkolenie na false i uruchom program. Potem ustaw maZgodeOpiekuna = false. Zobacz, jak każda zmiana wpływa na warunek końcowy — to najlepszy sposób, żeby poczuć różnicę między && a ||.
Na co uważać
| Kod | Problem |
|---|---|
if (x = 5) |
Jeden znak równości to przypisanie. W C# to błąd kompilacji — warunek musi być typu bool. |
18 <= wiek <= 65 |
Zapis matematyczny nie działa. Trzeba wiek >= 18 && wiek <= 65. |
if (liczba) |
W C# liczba nie jest wartością logiczną. Trzeba if (liczba != 0). |
a == b dla double |
Bywa zawodne. Porównuj przez tolerancję. |
tekst == "tak" dla danych od użytkownika |
Wpisanie Tak albo TAK da false. Użyj porównania ignorującego wielkość liter. |
if (a & b) zamiast && |
Zadziała dla wartości bool, ale bez skracania — obie strony zostaną sprawdzone. Traci się zabezpieczenie przed błędem. |
liczba / dzielnik > 10 && dzielnik != 0 |
Zła kolejność. Zabezpieczenie musi być pierwsze, inaczej ocena skrócona nie zdąży zadziałać. |
a || b && c bez nawiasów |
Kompiluje się, ale && wiąże mocniej — łatwo o pomyłkę w interpretacji. Postaw nawiasy. |
tekst1 < tekst2 |
Napisów nie da się porównać operatorem <. Służy do tego string.Compare. |
Zadania
Dla int a = 5, b = 10; i bool t = true, f = false; zapisz wynik każdej linii.
int a = 5, b = 10;
bool t = true, f = false;
Console.WriteLine(a < b && b < 20);
Console.WriteLine(a > b || b > 20);
Console.WriteLine(!(a == 5));
Console.WriteLine(t && f || t);
Console.WriteLine(a + 5 == b);
Console.WriteLine(a * 2 != b);
Zapisz w C# poniższe warunki, używając zmiennych wiek, ocena, maZaswiadczenie:
- wiek mieści się w przedziale od 15 do 19 lat włącznie
- ocena jest niedostateczna (mniejsza niż 2) albo brakuje zaświadczenia
- uczeń nie jest ani zbyt młody (poniżej 15), ani zbyt stary (powyżej 19)
Zastosuj prawa De Morgana, aby usunąć zewnętrzną negację z każdego wyrażenia:
!(zalogowany && maUprawnienia)
!(pusty || uszkodzony)
!(wiek >= 18 && maDokument)
Napisz warunek sprawdzający, czy rok jest przestępny. Zasada: rok jest przestępny, jeśli dzieli się przez 4, ale nie przez 100 — chyba że dzieli się przez 400. Sprawdź go dla lat 2024, 1900 i 2000.
Dla liczb a = 20 i b = 6 wypisz ich zapis dwójkowy oraz wyniki działań &, |, ^, a także a << 2 i a >> 2. Sprawdź, czy przesunięcia rzeczywiście odpowiadają mnożeniu i dzieleniu przez 4.
Zbuduj maskę bitową dla czterech uprawnień: odczyt (1), zapis (2), usuwanie (4), udostępnianie (8). Nadaj użytkownikowi odczyt i udostępnianie, sprawdź wszystkie cztery uprawnienia, następnie odbierz udostępnianie i sprawdź ponownie.
Co trzeba zapamiętać
- Porównania w tych przykładach zwracają bool. = przypisuje, a == porównuje.
- Przedział zapisujemy jako dwa porównania połączone &&.
- && i || stosują ocenę skróconą. Zabezpieczenie umieszczaj przed operacją, której może nie wolno wykonać.
- && ma wyższy priorytet niż ||. Nawiasy pomagają pokazać zamierzony sens.
- Dla napisów == porównuje treść z rozróżnianiem wielkości liter. Jawny StringComparison pozwala określić sposób porównania.
- Operatory & i | mają zastosowanie zarówno do liczb bitowych, jak i bool; dla bool obliczają obie strony.
- Przesunięcia bitowe mogą usuwać bity. Równoważność z mnożeniem lub dzieleniem wymaga uwzględnienia zakresu i znaku.
- Prawa De Morgana pozwalają poprawnie negować złożone warunki.