Pętle to mycie talerzy
Mycie talerzy - powtarzasz ten sam ruch, dopóki zlew nie jest pusty. Tyle.
Zlew pełen talerzy #
Wyobraź sobie zlew pełen brudnych talerzy po niedzielnym obiedzie u babci. Ktoś musi to umyć. Co robisz?
- Bierzesz jeden talerz.
- Myjesz go.
- Odkładasz na suszarkę.
- Sprawdzasz: czy w zlewie coś jeszcze zostało?
- Jeśli tak - wracasz do kroku 1.
To jest pętla. Tyle. Nie ma w tym żadnej magii.
Trzy rzeczy, które musisz wiedzieć #
Każda pętla - nieważne w jakim języku - potrzebuje trzech rzeczy, żeby nie oszaleć:
- Punkt startu - „weź pierwszy talerz”. Inaczej nie wiesz, skąd zacząć.
- Warunek przerwania - „myj, dopóki zlew nie jest pusty”. Bez tego myjesz w nieskończoność.
- Ruch do przodu - „po umyciu talerza, sięgnij po następny”. Bez tego myjesz w kółko ten sam talerz.
Zapomnij chociaż jedno, a masz pętlę nieskończoną - komputer próbuje umyć jeden talerz przez wieczność, aż karta graficzna się rozpłacze.
W kodzie #
const talerze = ['obiadowy', 'deserowy', 'filiżanka'];
for (const talerz of talerze) {
console.log(`Myję: ${talerz}`);
}
Pamiętaj: Pętla to nie jest „zaawansowana technika”. To mycie talerzy. Jak umiesz umyć zlew po obiedzie, umiesz napisać pętlę.
Kiedy uważać #
Jeśli w pracy napiszesz pętlę, która ma milion talerzy i każdy myjesz przez sekundę - przeglądarka zamrozi się na 17 minut. Zanim napiszesz pętlę, zadaj pytanie: ile talerzy naprawdę będzie w zlewie?
Cztery konstrukcje, jeden cel #
Pętla to sposób na wykonanie tego samego kodu wiele razy. W C# masz kilka wariantów, różnią się nie tyle co robią, co jak wygodnie i szybko:
for- indeksowana iteracja, znasz start, koniec i krok.foreach- iteracja poIEnumerable<T>, nie musisz znać rozmiaru.while/do-while- iteracja do spełnienia warunku, bez z góry znanej liczby kroków.- LINQ / metody wyższego rzędu -
Select,Where,Aggregate- deklaratywne, pod spodem i tak iterują.
Anatomia pętli - trzy elementy #
Każda pętla (nawet ta opakowana w LINQ) musi mieć te trzy rzeczy, inaczej się rozsypuje:
- Stan początkowy - np. indeks
i = 0, iteratorIEnumerator<T>. - Warunek kontynuacji -
i < list.Count,enumerator.MoveNext(). - Aktualizację stanu -
i++,enumeratorprzesuwa się do następnego elementu.
Pominięcie aktualizacji = nieskończona pętla. while (true) bez break to legalne użycie (event loop, serwer), ale musisz mieć inną drogę wyjścia.
for vs foreach - co kosztuje #
// title: LoopBench.cs
var list = Enumerable.Range(0, 1_000_000).ToList();
// for - indeksowany dostęp, best dla List<T>
long sum = 0;
for (int i = 0; i < list.Count; i++)
sum += list[i];
// foreach - przez IEnumerator<T>, alokuje enumerator (struct w List<T>, klasa w IEnumerable<T>)
long sum2 = 0;
foreach (var x in list)
sum2 += x;
Dla List<T> foreach jest zoptymalizowany (struct enumerator, nie alokuje na stercie). Dla IEnumerable<T> już tak - dostajesz boxing i GC pressure przy tight loopach.
LINQ - wygoda kosztem alokacji #
// Deklaratywnie
var sum = list.Where(x => x % 2 == 0).Select(x => x * 2).Sum();
Pod spodem: każdy operator to nowy iterator (klasa), każde lambda to delegate. Dla 1000 elementów niezauważalne. Dla 10M w hot path - mierzalne. BenchmarkDotNet + MemoryDiagnoser pokaże.
Pętle równoległe #
Gdy operacja na elemencie jest niezależna od innych, można rozbić iterację na wątki:
// PLINQ
var sum = list.AsParallel().Sum();
// Parallel.ForEach - bardziej kontroli
Parallel.ForEach(list, item => ProcessItem(item));
Zasady: mierz przed. Parallel ma narzut (task scheduling, partitioning). Dla taniej operacji (< ~1 us / element) sekwencyjne jest szybsze. Dla I/O nie używaj Parallel.ForEach - to biblioteka dla CPU-bound. Przy I/O - await Task.WhenAll.
Złożoność - licz zanim napiszesz #
Pętla w pętli = O(n²). Dla n = 10_000: sto milionów operacji. Przy nanosekundzie na operację - 100 ms. Przy milisekundzie (np. zapytanie do bazy w środku) - 100 sekund.
Najczęstszy bug produkcyjny początkującego: N+1 queries.
// title: N+1 - zły wzorzec
foreach (var user in users) // 1 query: SELECT * FROM users
{
var orders = db.Orders.Where(o => o.UserId == user.Id).ToList(); // N queries
// ...
}
Fix: JOIN lub Include / projekcja:
var result = db.Users
.Include(u => u.Orders)
.ToList(); // 1 query z JOIN
Wczesne wyjście - break, return, continue #
break- przerywa całą pętlę.continue- pomija resztę iteracji, przechodzi do warunku.return- wychodzi z metody, też z pętli.
W LINQ odpowiednikami są First, Any, TakeWhile - zatrzymują iterację gdy warunek spełniony (są lazy).
Wzorce, które warto znać #
- Sliding window - dwie zmienne (
left,right), obie przesuwają się warunkowo. Linear scan zamiast zagnieżdżonegofor. - Two pointers - dla posortowanych danych, od końca i początku.
- State machine -
switch+ zmienna stanu wewnątrzwhile. - Enumerator dla strumieni -
IAsyncEnumerable<T>(od C# 8) dla asynchronicznej iteracji (np. streaming z bazy, SignalR).
Skrótowo #
Pętla to kontrakt: warunek początkowy + warunek stopu + postęp. Zmień dowolną z tych rzeczy na podejrzaną, a dostaniesz buga. Dla większości pracy z kolekcjami w C# używaj foreach (czytelność), LINQ tam gdzie agregujesz / filtrujesz, for gdy potrzebujesz indeksu lub performance, while gdy nie znasz liczby iteracji z góry.
Podobne wpisy
Komputer to bardzo szybki, ale całkowicie dosłowny wykonawca
Dlaczego kod musi być pisany z dokładnością, której nigdy nie żądamy od człowieka - i co z tego wynika dla całej branży.
Rekurencja i matrioszki
Każda laleczka zawiera mniejszą wersję samej siebie - aż do tej, której nie da się otworzyć.
LINQ w C# - lambdy, Where, Select i leniwa ewaluacja
Wyrażenia lambda, operator zapytań LINQ, łączenie metod w łańcuchy oraz pułapka leniwej ewaluacji - jak filtrować, sortować i przekształcać kolekcje jednym, czytelnym łańcuchem wywołań. Czwarta część kursu C#.
🔗 Linkują tu