Do tej pory pisaliśmy kod który wykonywał się linia po linii, od góry do dołu. Ale prawdziwe programy muszą podejmować decyzje ("jeśli użytkownik jest zalogowany, pokaż dashboard") i powtarzać operacje ("dla każdego produktu, oblicz cenę"). Do tego służy kontrola przepływu programu!
W tym wpisie nauczysz się if/else z pattern matching, rewolucyjnych switch expressions (C# 8+), wszystkich rodzajów pattern matching (C# 7-14), oraz pętli - for, foreach, while. Zapomnij o starym switch statement - poznasz nowoczesny sposób!
🔍 Czym jest kontrola przepływu?
Kontrola przepływu to sposób w jaki decydujesz co program ma robić:
int age = 20;
// Prosty if
if (age >= 18)
{
Console.WriteLine("Pełnoletni");
}
// if-else
if (age >= 18)
{
Console.WriteLine("Pełnoletni");
}
else
{
Console.WriteLine("Niepełnoletni");
}
// if-else if-else
if (age < 13)
{
Console.WriteLine("Dziecko");
}
else if (age < 18)
{
Console.WriteLine("Nastolatek");
}
else if (age < 65)
{
Console.WriteLine("Dorosły");
}
else
{
Console.WriteLine("Senior");
}
// Bez nawiasów klamrowych (tylko dla JEDNEJ instrukcji)
if (age >= 18)
Console.WriteLine("Pełnoletni"); // tylko jedna linia - OK
else
Console.WriteLine("Niepełnoletni");
⚠️ Zawsze używaj {} nawet dla jednej linii!
// ❌ Bez nawiasów - podatne na błędy!
if (age >= 18)
Console.WriteLine("Pełnoletni");
Console.WriteLine("Może głosować"); // To ZAWSZE się wykona! (nie jest w if)
// ✅ Z nawiasami - bezpieczne
if (age >= 18)
{
Console.WriteLine("Pełnoletni");
Console.WriteLine("Może głosować"); // Oba w if
}
99% C# devs używa ZAWSZE nawiasów. To zapobiega błędom i jest bardziej czytelne. 👍
Operatory logiczne w warunkach
int age = 25;
bool isStudent = true;
string country = "Poland";
// && - AND (i)
if (age >= 18 && age <= 65)
{
Console.WriteLine("Wiek produkcyjny");
}
// || - OR (lub)
if (age < 18 || age > 65)
{
Console.WriteLine("Poza wiekiem produkcyjnym");
}
// ! - NOT (negacja)
if (!isStudent)
{
Console.WriteLine("Nie jest studentem");
}
// Kombinacje
if (age >= 18 && (isStudent || country == "Poland"))
{
Console.WriteLine("Może otrzymać zniżkę");
}
// Porównanie stringów - case insensitive
if (country.Equals("poland", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Polska!");
}
Null checking - różne sposoby
string? name = GetUserName(); // może być null
// 1. Klasyczny null check
if (name != null)
{
Console.WriteLine(name.ToUpper());
}
// 2. Null-conditional operator (C# 6+)
Console.WriteLine(name?.ToUpper()); // null jeśli name jest null
// 3. is not null (C# 9+) - czytelniejsze!
if (name is not null)
{
Console.WriteLine(name.ToUpper());
}
// 4. Pattern matching (C# 7+) - z deklaracją zmiennej
if (name is string validName)
{
Console.WriteLine(validName.ToUpper()); // validName to string (nie nullable)
}
// 5. IsNullOrWhiteSpace dla stringów
if (!string.IsNullOrWhiteSpace(name))
{
Console.WriteLine(name.ToUpper());
}
❌ Stary sposób (2015)
string name = GetName();
if (name != null)
{
if (name.Length > 0)
{
Console.WriteLine(name);
}
}
// Zagnieżdżone if'y - brzydkie!
✅ Nowoczesny sposób (2026)
string? name = GetName();
// Pattern matching + is
if (name is { Length: > 0 })
{
Console.WriteLine(name);
}
// Lub guard clause (więcej o tym później)
if (name is null or { Length: 0 }) return;
Console.WriteLine(name);
Ternary operator - skrócony if
int age = 20;
// Pełny if-else
string status;
if (age >= 18)
{
status = "Dorosły";
}
else
{
status = "Niepełnoletni";
}
// Ternary operator - condition ? valueIfTrue : valueIfFalse
string status2 = age >= 18 ? "Dorosły" : "Niepełnoletni";
// Zagnieżdżony ternary (NIE NADUŻYWAJ!)
string category = age < 13 ? "Dziecko"
: age < 18 ? "Nastolatek"
: age < 65 ? "Dorosły"
: "Senior";
// Użycie w wyrażeniach
int discount = isStudent ? 20 : 0;
Console.WriteLine($"Zniżka: {discount}%");
// W LINQ
var adults = users.Where(u => u.Age >= 18 ? true : false); // ❌ niepotrzebne!
var adults2 = users.Where(u => u.Age >= 18); // ✅ wystarczy warunek
Pattern Matching - potęga rozpoznawania wzorców
Type pattern - sprawdzanie typu
object obj = "Hello";
// Type pattern - is Type variableName
if (obj is string text)
{
Console.WriteLine($"Długość: {text.Length}");
// 'text' jest typu string (nie object)
}
// Działa z wieloma typami
object value = 42;
if (value is int number)
{
Console.WriteLine($"Liczba: {number}");
}
else if (value is string str)
{
Console.WriteLine($"String: {str}");
}
else if (value is bool flag)
{
Console.WriteLine($"Bool: {flag}");
}
// Negative pattern - is not (C# 9+)
if (obj is not string)
{
Console.WriteLine("To nie jest string");
}
Constant pattern - stałe wartości
int number = 42;
// Constant pattern
if (number is 42)
{
Console.WriteLine("Answer to everything!");
}
// Null pattern
string? name = null;
if (name is null)
{
Console.WriteLine("Name is null");
}
// Not null (C# 9+)
if (name is not null)
{
Console.WriteLine($"Name: {name}");
}
Property pattern - sprawdzanie właściwości
record Person(string Name, int Age, string City);
Person person = new("Jan", 30, "Warszawa");
// Property pattern - { PropertyName: pattern }
if (person is { Age: 30 })
{
Console.WriteLine("Ma 30 lat");
}
// Wiele właściwości
if (person is { Age: 30, City: "Warszawa" })
{
Console.WriteLine("30-latek z Warszawy");
}
// Z relational patterns (C# 9+)
if (person is { Age: >= 18 and < 65 })
{
Console.WriteLine("Wiek produkcyjny");
}
// Nested properties
record Address(string City, string Street);
record Employee(string Name, Address Address);
Employee emp = new("Jan", new("Warszawa", "Marszałkowska"));
if (emp is { Address.City: "Warszawa" })
{
Console.WriteLine("Pracownik z Warszawy");
}
Relational patterns - porównania (C# 9+)
int age = 25;
// Relational patterns: <,>, <=, >=
if (age is >= 18)
{
Console.WriteLine("Dorosły");
}
if (age is > 0 and < 18)
{
Console.WriteLine("Niepełnoletni");
}
// Kombinacje z logical patterns
if (age is >= 18 and < 65)
{
Console.WriteLine("Wiek produkcyjny");
}
if (age is < 13 or > 65)
{
Console.WriteLine("Zniżka seniorska/dziecięca");
}
// Not pattern
if (age is not 0)
{
Console.WriteLine("Wiek określony");
}
Logical patterns - and, or, not (C# 9+)
int number = 42;
// and
if (number is > 0 and < 100)
{
Console.WriteLine("Liczba od 1 do 99");
}
// or
if (number is 0 or 100)
{
Console.WriteLine("Graniczna wartość");
}
// not
if (number is not 0)
{
Console.WriteLine("Niezerowa");
}
// Kombinacje
string? text = "Hello";
if (text is not null and { Length: > 0 })
{
Console.WriteLine("Niepusty string");
}
// Zaawansowane
int value = 50;
if (value is (> 0 and < 25) or (> 75 and < 100))
{
Console.WriteLine("Pierwszy lub ostatni kwartyl");
}
List patterns - wzorce list (C# 11+)
int[] numbers = { 1, 2, 3, 4, 5 };
// List pattern - [element1, element2, ...]
if (numbers is [1, 2, 3, 4, 5])
{
Console.WriteLine("Dokładnie te liczby");
}
// Discard pattern - _ (ignoruj element)
if (numbers is [1, _, 3, _, 5])
{
Console.WriteLine("1, coś, 3, coś, 5");
}
// Slice pattern - .. (reszta elementów)
if (numbers is [1, 2, ..])
{
Console.WriteLine("Zaczyna się od 1, 2");
}
if (numbers is [.., 4, 5])
{
Console.WriteLine("Kończy się na 4, 5");
}
if (numbers is [1, .., 5])
{
Console.WriteLine("Pierwszy = 1, ostatni = 5");
}
// Var pattern - złap resztę
if (numbers is [1, 2, .. var rest])
{
Console.WriteLine($"Pierwsze 2: 1, 2. Reszta: {string.Join(", ", rest)}");
// rest = { 3, 4, 5 }
}
// Length pattern
if (numbers is { Length: 5 })
{
Console.WriteLine("Tablica ma 5 elementów");
}
💡 Praktyczne przykłady pattern matching
// Przykład 1: Walidacja użytkownika
record User(string Name, int Age, bool IsActive);
User user = new("Jan", 25, true);
if (user is { Age: >= 18, IsActive: true })
{
Console.WriteLine("Użytkownik może się zalogować");
}
// Przykład 2: Parsing różnych typów
object value = GetValue();
string description = value switch
{
null => "Null",
int n when n > 0 => $"Dodatnia liczba: {n}",
int n when n < 0 => $"Ujemna liczba: {n}",
int => "Zero",
string { Length: 0 } => "Pusty string",
string s => $"String: {s}",
_ => "Nieznany typ"
};
// Przykład 3: HTTP status codes
int statusCode = 404;
string message = statusCode switch
{
200 => "OK",
201 => "Created",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
>= 500 and < 600 => "Server Error",
_ => "Unknown status"
};
Switch Expression - rewolucja! (C# 8+)
Stary switch statement (NIE UŻYWAJ!)
// ❌ Stary switch statement (C# 1.0)
int dayNumber = 3;
string dayName;
switch (dayNumber)
{
case 1:
dayName = "Poniedziałek";
break;
case 2:
dayName = "Wtorek";
break;
case 3:
dayName = "Środa";
break;
case 4:
dayName = "Czwartek";
break;
case 5:
dayName = "Piątek";
break;
case 6:
case 7:
dayName = "Weekend";
break;
default:
dayName = "Nieprawidłowy dzień";
break;
}
// Verbose, wymaga break, łatwo o błędy!
Nowy switch expression (UŻYWAJ!)
🎉 NOWOŚĆ C# 8+ - Switch Expression
switch expression to nowoczesna, zwięzła forma switch. Zwraca wartość, nie potrzebuje break, wspiera pattern matching!
// Podstawowa pętla for
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Iteracja {i}");
}
// Output: 0, 1, 2, 3, 4
// Od tyłu
for (int i = 5; i > 0; i--)
{
Console.WriteLine(i);
}
// Output: 5, 4, 3, 2, 1
// Co drugi element
for (int i = 0; i < 10; i += 2)
{
Console.WriteLine(i);
}
// Output: 0, 2, 4, 6, 8
// Wiele zmiennych (rzadko używane)
for (int i = 0, j = 10; i < j; i++, j--)
{
Console.WriteLine($"i={i}, j={j}");
}
// Iteracja po tablicy/liście
int[] numbers = { 10, 20, 30, 40, 50 };
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"numbers[{i}] = {numbers[i]}");
}
foreach - pętla po kolekcji
// foreach - najprostsza pętla po kolekcji
int[] numbers = { 10, 20, 30, 40, 50 };
foreach (int number in numbers)
{
Console.WriteLine(number);
}
// Działa z każdą kolekcją
List names = ["Jan", "Anna", "Piotr"];
foreach (string name in names)
{
Console.WriteLine(name);
}
// Z var
foreach (var item in numbers)
{
Console.WriteLine(item); // item to int (wywnioskowane)
}
// Dictionary - foreach po parach
Dictionary ages = new()
{
["Jan"] = 30,
["Anna"] = 25
};
foreach (KeyValuePair pair in ages)
{
Console.WriteLine($"{pair.Key} ma {pair.Value} lat");
}
// C# 7+ - deconstruction
foreach (var (name, age) in ages)
{
Console.WriteLine($"{name} ma {age} lat");
}
❌ for gdy nie potrzebujesz indeksu
List fruits = ["Jabłko", "Banan", "Gruszka"];
for (int i = 0; i < fruits.Count; i++)
{
Console.WriteLine(fruits[i]);
}
// Verbose, podatne na błędy (można pomylić Length/Count)
✅ foreach gdy nie potrzebujesz indeksu
List fruits = ["Jabłko", "Banan", "Gruszka"];
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
// Czytelne, bezpieczne!
while - pętla z warunkiem
// while - wykonuj DOPÓKI warunek jest prawdziwy
int count = 0;
while (count < 5)
{
Console.WriteLine($"Count: {count}");
count++;
}
// Nieskończona pętla (wymaga break)
int userInput;
while (true)
{
Console.Write("Podaj liczbę (0 = koniec): ");
userInput = int.Parse(Console.ReadLine());
if (userInput == 0)
break; // wyjdź z pętli
Console.WriteLine($"Podałeś: {userInput}");
}
// do-while - wykonaj PRZYNAJMNIEJ RAZ, potem sprawdź warunek
int number;
do
{
Console.Write("Podaj liczbę dodatnią: ");
number = int.Parse(Console.ReadLine());
} while (number <= 0);
Console.WriteLine($"Dziękuję! Liczba: {number}");
break, continue, return
// break - przerwij pętlę natychmiast
for (int i = 0; i < 10; i++)
{
if (i == 5)
break; // przerwij gdy i == 5
Console.WriteLine(i);
}
// Output: 0, 1, 2, 3, 4 (zatrzymuje się na 5)
// continue - pomiń resztę iteracji, przejdź do następnej
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
continue; // pomiń parzyste
Console.WriteLine(i);
}
// Output: 1, 3, 5, 7, 9 (tylko nieparzyste)
// return - wyjdź z metody
void ProcessNumbers(int[] numbers)
{
foreach (int num in numbers)
{
if (num < 0)
{
Console.WriteLine("Znaleziono liczbę ujemną!");
return; // wyjdź z całej metody
}
Console.WriteLine(num);
}
}
// Kombinacja break i continue
List numbers = [1, -5, 3, -2, 8, 0, 10];
foreach (int num in numbers)
{
if (num == 0)
break; // zatrzymaj się na zero
if (num < 0)
continue; // pomiń ujemne
Console.WriteLine(num);
}
// Output: 1, 3 (zatrzymuje się na 0)
⚠️ Unikaj zagnieżdżonych break/continue
// ❌ Skomplikowane - trudne do zrozumienia
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
if (i == 5 && j == 5)
break; // break tylko z wewnętrznej pętli!
Console.WriteLine($"{i},{j}");
}
}
// ✅ Lepiej - wydziel do metody
for (int i = 0; i < 10; i++)
{
if (ProcessRow(i))
break; // jasne - break z głównej pętli
}
bool ProcessRow(int row)
{
for (int j = 0; j < 10; j++)
{
if (row == 5 && j == 5)
return true; // return = sygnał do break
Console.WriteLine($"{row},{j}");
}
return false;
}
Guard Clauses - obronna walidacja
Problem z zagnieżdżonymi if'ami
// ❌ Zagnieżdżone if'y - "Pyramid of Doom"
void ProcessOrder(Order order)
{
if (order != null)
{
if (order.Items.Count > 0)
{
if (order.Customer != null)
{
if (order.Customer.IsActive)
{
// Właściwa logika tu...
CalculateTotal(order);
}
else
{
Console.WriteLine("Customer is not active");
}
}
else
{
Console.WriteLine("Customer is null");
}
}
else
{
Console.WriteLine("No items in order");
}
}
else
{
Console.WriteLine("Order is null");
}
}
// Kod znika w prawo, trudno czytać! 😱
Rozwiązanie - Guard Clauses
// ✅ Guard clauses - early returns
void ProcessOrder(Order? order)
{
// Walidacja na początku - fail fast!
if (order is null)
{
Console.WriteLine("Order is null");
return;
}
if (order.Items.Count == 0)
{
Console.WriteLine("No items in order");
return;
}
if (order.Customer is null)
{
Console.WriteLine("Customer is null");
return;
}
if (!order.Customer.IsActive)
{
Console.WriteLine("Customer is not active");
return;
}
// Właściwa logika - bez zagnieżdżenia!
CalculateTotal(order);
}
// Płaski kod, łatwy do czytania! ✨
💡 Guard clauses z pattern matching
void ProcessPayment(Payment? payment)
{
// Guard clauses z pattern matching
if (payment is null or { Amount: <= 0 })
{
throw new ArgumentException("Invalid payment");
}
if (payment is { Status: PaymentStatus.Completed })
{
Console.WriteLine("Payment already processed");
return;
}
if (payment is { Account.Balance: < 0 })
{
Console.WriteLine("Insufficient funds");
return;
}
// Proces płatności
ProcessPaymentInternal(payment);
}
// Zaawansowane - kombination
record User(string Name, int Age, bool IsActive, string? Email);
void SendEmail(User? user)
{
// Wszystkie warunki w jednym guard
if (user is not { IsActive: true, Age: >= 18, Email: not null })
{
Console.WriteLine("Cannot send email");
return;
}
// user.Email jest na pewno not null tutaj!
SendEmailTo(user.Email);
}
Zaawansowane wzorce
Expression-bodied members
// Krótkie metody można zapisać jako expression
class Calculator
{
// Tradycyjna metoda
public int Add(int a, int b)
{
return a + b;
}
// Expression-bodied method (C# 6+)
public int Multiply(int a, int b) => a * b;
// Expression-bodied property
public int CurrentYear => DateTime.Now.Year;
// Expression-bodied readonly property
private int _value;
public int DoubleValue => _value * 2;
// Expression-bodied get/set (C# 7+)
private string _name;
public string Name
{
get => _name;
set => _name = value?.Trim() ?? "";
}
}
// Kiedy używać? Dla prostych, jednoliniowych operacji
// ✅ Dobrze:
public bool IsAdult(int age) => age >= 18;
public string FullName => $"{FirstName} {LastName}";
// ❌ Źle (za długie):
public decimal CalculateDiscount(Order order) =>
order.Items.Sum(i => i.Price) > 1000 ?
order.Customer.IsPremium ? 0.25m : 0.15m : 0.05m;
// To powinno być normalną metodą z {}
Null-coalescing assignment ??=
// ??= - przypisz jeśli null (C# 8+)
List? items = null;
// Stary sposób
if (items == null)
{
items = new List();
}
// Nowy sposób
items ??= new List();
// "Jeśli items jest null, przypisz new List()"
// Praktyczne użycie - lazy initialization
class UserService
{
private List? _cachedUsers;
public List GetUsers()
{
// Załaduj tylko raz
_cachedUsers ??= LoadUsersFromDatabase();
return _cachedUsers;
}
}
// Krótsze warianty
string? name = null;
name ??= "Default"; // name = "Default"
name = "John";
name ??= "Default"; // name nadal = "John" (nie null)
Throw expressions (C# 7+)
// throw można użyć jako wyrażenia (nie tylko instrukcji)
// W ternary operator
string GetName(string? input) =>
input ?? throw new ArgumentNullException(nameof(input));
// W null-coalescing
string name = userName ?? throw new InvalidOperationException("User not logged in");
// W switch expression
string GetStatusMessage(int code) => code switch
{
200 => "OK",
404 => "Not Found",
_ => throw new ArgumentException($"Unknown code: {code}")
};
// W expression-bodied member
class Person
{
private string _name;
public string Name
{
get => _name;
set => _name = !string.IsNullOrEmpty(value)
? value
: throw new ArgumentException("Name cannot be empty");
}
}
Podsumowanie
Ogromny wpis o kontroli przepływu! Nauczyłeś się wszystkiego o podejmowaniu decyzji i pętlach w C# 14: