1. Wprowadzenie
Zacznijmy od pytania: po co w ogóle wkładać jedną pętlę w drugą? Chodzi o to, że często nasze dane lub zadania są zorganizowane nie tylko w jednej linii, ale np. w tabeli, siatce albo nawet w wielowymiarowej strukturze. Załóżmy, że chcesz wyświetlić na ekranie tabliczkę mnożenia, przejść przez dwuwymiarową tablicę albo policzyć przecięcia między wszystkimi parami elementów. Tutaj jedna pętla to za mało — potrzebujesz pętli w pętli.
W programowaniu zagnieżdżona pętla to jak dwa budziki: zewnętrzny zaczyna dzwonić, a w środku niego uruchamia się jeszcze jeden, który będzie dzwonił za każdym razem, gdy pierwszy jest aktywny. Tak więc, gdy trwa jedna "zewnętrzna" iteracja, wewnętrzna przechodzi całą swoją drogę (i robi to znowu i znowu dla każdej iteracji zewnętrznej).
Dobry przykład — godziny i minuty. Godziny — to zewnętrzna pętla od 0 do 23, minuty — wewnętrzna pętla od 0 do 59. Przy każdej zmianie zewnętrznej pętli wewnętrzna zdąży przelecieć przez wszystkie swoje wartości.
2. Składnia zagnieżdżonych pętli
W C# składnia zagnieżdżonych pętli niczym się nie różni od zwykłych pętli — po prostu piszesz jedną pętlę wewnątrz ciała drugiej. Zobaczmy przykłady z for i while:
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
Console.Write($"{i},{j} ");
}
Console.WriteLine();
}
Tutaj pętla zewnętrzna kontroluje zmienną i (od 0 do 2), a wewnętrzna — zmienną j (od 0 do 3). Dla każdej wartości i pętla wewnętrzna przechodzi w całości od j=0 do j=3. Jeśli uruchomisz ten kod, zobaczysz fajną tabelkę współrzędnych:
0,0 0,1 0,2 0,3
1,0 1,1 1,2 1,3
2,0 2,1 2,2 2,3
Podobny przykład z użyciem while:
int i = 0;
while (i < 3)
{
int j = 0;
while (j < 4)
{
Console.Write($"{i},{j} ");
j++;
}
Console.WriteLine();
i++;
}
Zwróć uwagę: w każdej iteracji zewnętrznej pętli zmienna pętli wewnętrznej (j) musi być zainicjalizowana od nowa, inaczej zobaczysz tylko jeden wiersz!
3. Przykłady działania zagnieżdżonych pętli
Przykład 1: wyświetlanie szachownicy (8x8)
Niech naszym pierwszym zadaniem będzie wyświetlenie na ekranie klasycznej szachownicy w postaci czarnych i białych pól (niech # — czarne, _ — białe). Zróbmy to za pomocą zagnieżdżonych pętli for:
for (int row = 0; row < 8; row++)
{
for (int col = 0; col < 8; col++)
{
if ((row + col) % 2 == 0)
Console.Write("_");
else
Console.Write("#");
}
Console.WriteLine();
}
Wynik:
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
_#_#_#_#
#_#_#_#_
Ważny moment: zagnieżdżenie zapewnia, że dla każdego wiersza (row) przechodzimy w całości przez wszystkie kolumny (col). Bez zagnieżdżenia nie dostalibyśmy struktury planszy — tylko jeden wiersz albo jedną kolumnę.
Przykład 2: tabliczka mnożenia
Jeden z klasycznych kandydatów na zagnieżdżone pętle! Wyświetlmy tabliczkę mnożenia 1-9:
for (int i = 1; i <= 9; i++)
{
for (int j = 1; j <= 9; j++)
{
Console.Write($"{i * j,3} ");
}
Console.WriteLine();
}
Formatowanie ${i * j,3} dodaje odstępy, żeby tabela wyglądała schludnie.
Wynik:
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
...
9 18 27 36 45 54 63 72 81
4. Zagnieżdżone pętle i sterowanie nimi — niuanse
O wpływie break i continue w zagnieżdżonych pętlach
Tu wielu początkujących się łapie! Jeśli używasz break albo continue w pętli wewnętrznej, wpływają one tylko na tę pętlę. Zewnętrzna działa dalej jak gdyby nigdy nic.
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
if (j == 3)
break; // wychodzimy tylko z pętli wewnętrznej!
Console.Write($"{i},{j} ");
}
Console.WriteLine();
}
Wynik:
0,0 0,1 0,2
1,0 1,1 1,2
2,0 2,1 2,2
Jeśli musisz wyjść od razu z dwóch zagnieżdżonych pętli (np. natychmiast zakończyć szukanie przy pierwszym trafieniu w tabeli), zwykle używa się flagi lub specjalnego triku (np. return — jeśli wszystko dzieje się w funkcji).
5. Wizualizacja zagnieżdżonych pętli
Czasem trudno "zobaczyć" kolejność wykonywania zagnieżdżonych pętli. Spójrz na ten schemat blokowy:
W formie tabelarycznej — ile będzie wszystkich iteracji przy i od 1 do 3, j od 1 do 4?
| i | j (przeglądane dla każdego i) | Iteracji pętli wewnętrznej |
|---|---|---|
| 1 | 1, 2, 3, 4 | 4 |
| 2 | 1, 2, 3, 4 | 4 |
| 3 | 1, 2, 3, 4 | 4 |
| Razem: 3 × 4 = 12 |
6. Błędy i pułapki przy pracy z zagnieżdżonymi pętlami
Popularny błąd — niepoprawna inicjalizacja zmiennej pętli wewnętrznej. Na przykład zadeklarować ją poza pętlą zewnętrzną, ale nie resetować przy każdym kroku. W efekcie pętla wewnętrzna może się w ogóle nie wykonać, albo wykonać się źle.
int j = 0;
for (int i = 0; i < 3; i++)
{
while (j < 4) // Ups! j mogło już być równe 4 po pierwszej iteracji.
{
Console.Write($"{i},{j} ");
j++;
}
Console.WriteLine();
}
Tutaj pętla wykona się tylko raz. Nie zapominaj inicjalizować zmiennych pętli wewnętrznych wewnątrz pętli zewnętrznych!
Również, jeśli przypadkowo napiszesz dwie zagnieżdżone pętle z tymi samymi zmiennymi, kompilator się obrazi: zmienna już została zadeklarowana.
for (int i = 0; i < 3; i++)
{
for (int i = 0; i < 3; i++)
{
Console.Write($"{i},{i} ");
}
Console.WriteLine();
}
GO TO FULL VERSION