break, continue i pętle zagnieżdżone
Pętla nie zawsze musi dobiec do końca. Czasem trzeba wyjść, gdy znajdziemy to, czego szukamy, albo pominąć jeden obieg i lecieć dalej. A gdy pętla znajdzie się wewnątrz innej pętli — trzeba dokładnie wiedzieć, której z nich dotyczy nasze polecenie.
Czego się dziś nauczysz
- Przerwiesz pętlę w momencie znalezienia szukanej wartości
- Pominiesz jeden obieg pętli bez przerywania całości
- Wyjaśnisz, dlaczego
breakwswitchwewnątrz pętli nie kończy pętli - Napiszesz pętlę w pętli i policzysz, ile razy wykona się blok wewnętrzny
- Wyjdziesz z dwóch pętli naraz, nie używając
goto
Instrukcja break
break natychmiast kończy pętlę. Program przeskakuje za jej klamrę zamykającą i leci dalej — pozostałe obiegi się nie wykonają.
for (int i = 1; i <= 10; i++)
{
if (i == 4)
{
Console.WriteLine("Znalazlem czworke - koncze.");
break;
}
Console.WriteLine($"Sprawdzam {i}");
}
Console.WriteLine("Po petli.");
Sprawdzam 1
Sprawdzam 2
Sprawdzam 3
Znalazlem czworke - koncze.
Po petli.
Pętla miała wykonać dziesięć obiegów, wykonała cztery. Liczby od 5 do 10 nie zostały nawet sprawdzone.
Typowe zastosowanie: przerwij, gdy znalazłeś
To najczęstszy powód użycia break. Gdy szukasz jednej wartości, nie ma sensu przeglądać reszty:
string tekst = "programowanie";
char szukany = 'm';
int pozycja = -1; // -1 oznacza "nie znaleziono"
for (int i = 0; i < tekst.Length; i++)
{
if (tekst[i] == szukany)
{
pozycja = i;
break; // dalej szukac nie ma po co
}
}
if (pozycja >= 0)
{
Console.WriteLine($"Znak '{szukany}' pierwszy raz na pozycji {pozycja}.");
}
else
{
Console.WriteLine($"Znaku '{szukany}' nie ma w tekscie.");
}
Znak 'm' pierwszy raz na pozycji 6.
Dlaczego pozycja zaczyna od −1
Potrzebna jest wartość, która na pewno nie może być prawdziwym wynikiem. Pozycje w napisie numerujemy od zera, więc zero jest zajęte — jest poprawną odpowiedzią „pierwszy znak”. Minus jeden nigdy poprawną pozycją nie będzie, więc bezpiecznie oznacza „nie znaleziono”. Tak samo działa wbudowana metoda tekst.IndexOf(szukany).
Instrukcja continue
continue kończy bieżący obieg, ale nie pętlę. Program pomija resztę bloku i wraca na początek — do zwiększenia licznika i sprawdzenia warunku.
for (int i = 1; i <= 10; i++)
{
if (i % 2 == 0)
{
continue; // parzyste pomijamy
}
Console.WriteLine($"Nieparzysta: {i}");
}
Nieparzysta: 1
Nieparzysta: 3
Nieparzysta: 5
Nieparzysta: 7
Nieparzysta: 9
| Instrukcja | Co przerywa | Dokąd skacze |
|---|---|---|
break | całą pętlę | za klamrę zamykającą pętli |
continue | tylko bieżący obieg | do warunku (w for — najpierw do części zwiększającej licznik) |
continue w pętli while potrafi zawiesić program
W pętli for licznik zwiększa się zawsze, także przy continue — to część nagłówka pętli. W pętli while licznik zwiększasz ręcznie, więc continue postawione przed i++ pominie tę linię i pętla zatrzyma się na zawsze.
int i = 1;
while (i <= 10)
{
if (i % 2 == 0)
{
continue; // i nigdy nie wzrosnie - petla nieskonczona
}
Console.WriteLine(i);
i++;
}
W pętlach while bezpieczniej jest odwrócić warunek i nie używać continue w ogóle:
int i = 1;
while (i <= 10)
{
if (i % 2 != 0) // zamiast "pomijaj parzyste" - "wypisuj nieparzyste"
{
Console.WriteLine(i);
}
i++;
}
Kiedy continue naprawdę pomaga
Wtedy, gdy pozwala uniknąć głębokiego wcięcia. Zamiast owijać sto linii w if (dane_sa_dobre) { ... }, na początku pętli piszemy if (!dane_sa_dobre) { continue; } i reszta bloku zostaje na płaskim poziomie. Ten wzorzec nazywa się „wczesnym wyjściem” i naprawdę poprawia czytelność.
Pułapka: break w switch wewnątrz pętli
To jedno z najczęstszych nieporozumień u osób, które właśnie poznały switch. Instrukcja break odnosi się do najbliższej otaczającej konstrukcji — a jeśli jest nią switch, to pętla nawet nie drgnie.
while (true)
{
Console.Write("Polecenie: ");
string p = (Console.ReadLine() ?? "").Trim().ToLower();
switch (p)
{
case "pomoc":
Console.WriteLine("Dostepne: pomoc, koniec");
break; // wychodzi ze switch
case "koniec":
break; // TEZ wychodzi tylko ze switch!
default:
Console.WriteLine("Nie znam takiego polecenia.");
break;
}
}
Program nigdy się nie zakończy: break w gałęzi „koniec” wychodzi ze switch, po czym pętla while (true) zaczyna kolejny obieg. Rozwiązanie — flaga sprawdzana przez pętlę:
bool dziala = true;
while (dziala)
{
Console.Write("Polecenie: ");
string p = (Console.ReadLine() ?? "").Trim().ToLower();
switch (p)
{
case "pomoc":
Console.WriteLine("Dostepne: pomoc, koniec");
break;
case "koniec":
dziala = false; // to zakonczy petle
Console.WriteLine("Do zobaczenia.");
break;
default:
Console.WriteLine("Nie znam takiego polecenia.");
break;
}
}
Warunek wraca na swoje miejsce
Przy okazji zniknęło while (true). Nagłówek pętli mówi teraz wprost, kiedy program działa — nie trzeba szukać w środku, gdzie jest wyjście. To zwykle lepszy zapis.
Pętle zagnieżdżone
Wewnątrz pętli może stać kolejna pętla. Zasada jest jedna i wystarczy ją raz zrozumieć: na każdy jeden obieg pętli zewnętrznej pętla wewnętrzna przebiega całą swoją drogę od początku do końca.
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"Zewnetrzna: i = {i}");
for (int j = 1; j <= 2; j++)
{
Console.WriteLine($" wewnetrzna: j = {j}");
}
}
Zewnetrzna: i = 1
wewnetrzna: j = 1
wewnetrzna: j = 2
Zewnetrzna: i = 2
wewnetrzna: j = 1
wewnetrzna: j = 2
Zewnetrzna: i = 3
wewnetrzna: j = 1
wewnetrzna: j = 2
Pętla zewnętrzna wykonała się 3 razy, wewnętrzna — 3 × 2 = 6 razy. To mnożenie jest ważne: dwie pętle po 1000 obiegów to milion wykonań bloku wewnętrznego.
Klasyczne zastosowanie: tabela
Pętla zewnętrzna odpowiada za wiersze, wewnętrzna za kolumny wewnątrz wiersza. Kluczowa jest linia po pętli wewnętrznej — to ona kończy wiersz:
// naglowek
Console.Write(" ");
for (int j = 1; j <= 10; j++)
{
Console.Write($"{j,5}");
}
Console.WriteLine();
Console.WriteLine(new string('-', 54));
// wiersze
for (int i = 1; i <= 10; i++)
{
Console.Write($"{i,3}|"); // etykieta wiersza, bez nowej linii
for (int j = 1; j <= 10; j++)
{
Console.Write($"{i * j,5}"); // Write, nie WriteLine!
}
Console.WriteLine(); // dopiero tu konczymy wiersz
}
1 2 3 4 5 6 7 8 9 10
------------------------------------------------------
1| 1 2 3 4 5 6 7 8 9 10
2| 2 4 6 8 10 12 14 16 18 20
3| 3 6 9 12 15 18 21 24 27 30
4| 4 8 12 16 20 24 28 32 36 40
5| 5 10 15 20 25 30 35 40 45 50
6| 6 12 18 24 30 36 42 48 54 60
7| 7 14 21 28 35 42 49 56 63 70
8| 8 16 24 32 40 48 56 64 72 80
9| 9 18 27 36 45 54 63 72 81 90
10| 10 20 30 40 50 60 70 80 90 100
Write wewnątrz, WriteLine na zewnątrz
Najczęstszy błąd przy tabelach: użycie Console.WriteLine w pętli wewnętrznej. Wtedy każda liczba ląduje w osobnej linii i zamiast tabeli dostajesz stumetrową kolumnę. Nowa linia należy do wiersza, więc jej miejsce jest po pętli wewnętrznej.
Gdy wewnętrzna pętla zależy od zewnętrznej
Warunek pętli wewnętrznej może korzystać ze zmiennej pętli zewnętrznej. Tak rysuje się trójkąty:
int wysokosc = 5;
for (int i = 1; i <= wysokosc; i++)
{
for (int j = 1; j <= i; j++) // uwaga: warunek to j <= i
{
Console.Write('*');
}
Console.WriteLine();
}
*
**
***
****
*****
W pierwszym wierszu pętla wewnętrzna wykona się raz, w drugim dwa razy, i tak dalej. Ten sam efekt można osiągnąć jedną linią — Console.WriteLine(new string('*', i)); — i w prawdziwym programie tak właśnie byłoby lepiej. Wersja z pętlą służy do zrozumienia mechanizmu.
Wyjście z dwóch pętli naraz
break kończy tylko tę pętlę, w której stoi — najbliższą. Gdy szukamy czegoś w tabeli i chcemy przerwać całe przeszukiwanie, jedno break nie wystarczy:
for (int i = 1; i <= 3; i++)
{
for (int j = 1; j <= 3; j++)
{
if (i * j == 4)
{
Console.WriteLine($"Znalazlem: {i} x {j}");
break; // wychodzi tylko z petli po j
}
}
// ...i tu petla po i leci dalej
}
Najczytelniejsze rozwiązanie to flaga — zmienna logiczna sprawdzana także przez pętlę zewnętrzną:
bool znaleziono = false;
for (int i = 1; i <= 3 && !znaleziono; i++)
{
for (int j = 1; j <= 3; j++)
{
if (i * j == 4)
{
Console.WriteLine($"Znalazlem: {i} x {j}");
znaleziono = true;
break; // konczy petle wewnetrzna
}
}
// warunek zewnetrznej sprawdzi flage i tez sie zakonczy
}
if (!znaleziono)
{
Console.WriteLine("Nie znaleziono.");
}
Znalazlem: 2 x 2
Istnieje też goto — i lepiej z niego nie korzystać
C# ma instrukcję goto etykieta, którą dałoby się wyskoczyć z dowolnej liczby pętli. Działa, ale burzy porządek programu tak bardzo, że w praktyce jej nie używamy. Docelowym rozwiązaniem jest wydzielenie przeszukiwania do osobnej metody i zakończenie jej instrukcją return — pokażemy to w bloku o funkcjach.
Kiedy break pomaga, a kiedy szkodzi
| Dobre użycie | Podejrzane użycie |
|---|---|
| Szukasz pierwszego pasującego elementu i po znalezieniu nie masz już nic do roboty. | Kilka instrukcji break w jednej pętli, w różnych miejscach — nie da się już powiedzieć, kiedy pętla się kończy. |
| Sprawdzasz warunek, którego nie da się zapisać w nagłówku pętli (bo zależy od danych z wnętrza obiegu). | break tam, gdzie wystarczyłby warunek w nagłówku while — wtedy nagłówek kłamie o tym, jak długo pętla działa. |
| Przerywasz kosztowne obliczenia, gdy dalszy ciąg nie ma sensu (np. liczba okazała się złożona). | break jako sposób na wyjście z sytuacji, w której program i tak nie wie, co robić — wtedy problem jest gdzie indziej. |
Prosta zasada na start: jeden break w pętli to zwykle dobrze, trzy to znak, że pętlę trzeba przemyśleć od nowa.
Użyte w tej lekcji elementy
| Nazwa | Zwraca | Działanie |
|---|---|---|
tekst[i] | char |
Znak napisu o podanym numerze. Numery zaczynają się od zera, ostatni to tekst.Length - 1. Numer spoza zakresu przerywa program. |
tekst.IndexOf(z) | int |
Gotowa wersja przeszukiwania z tej lekcji: numer pierwszego wystąpienia znaku lub fragmentu, a -1, gdy nie ma. LastIndexOf szuka od końca. |
Console.Write(x) | nic | Wypisuje bez przechodzenia do nowej linii — podstawa budowania wierszy tabeli w pętli wewnętrznej. |
new string(znak, n) | string |
Napis z n powtórzeń znaku. Często zastępuje całą pętlę wewnętrzną. |
Math.Sqrt(x) | double |
Pierwiastek kwadratowy. W teście pierwszości wyznacza granicę, do której trzeba sprawdzać dzielniki. |
Environment.NewLine | string |
Znak (lub para znaków) końca linii właściwy dla systemu. Przydatny, gdy budujesz wielolinijkowy napis zamiast wypisywać go od razu. |
Liczby pierwsze do 50
Liczba pierwsza to taka większa od 1, która dzieli się bez reszty tylko przez 1 i przez samą siebie. Sprawdzenie wymaga pętli w pętli — i jest wzorcowym przykładem, w którym break naprawdę się opłaca.
const int Granica = 50;
int znalezione = 0;
Console.WriteLine($"Liczby pierwsze do {Granica}:");
Console.WriteLine();
for (int n = 2; n <= Granica; n++)
{
bool pierwsza = true;
// szukamy dzielnika; wystarczy sprawdzac do pierwiastka z n
for (int d = 2; d * d <= n; d++)
{
if (n % d == 0)
{
pierwsza = false;
break; // jeden dzielnik wystarczy, reszty nie sprawdzamy
}
}
if (!pierwsza)
{
continue; // n nie jest pierwsza - nastepna liczba
}
Console.Write($"{n,4}");
znalezione++;
if (znalezione % 10 == 0) // po dziesieciu liczbach lamiemy wiersz
{
Console.WriteLine();
}
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine($"Znaleziono {znalezione} liczb pierwszych.");
Liczby pierwsze do 50:
2 3 5 7 11 13 17 19 23 29
31 37 41 43 47
Znaleziono 15 liczb pierwszych.
Omówienie
| Fragment | Co robi i dlaczego tak |
|---|---|
bool pierwsza = true; |
Zakładamy niewinność. Pętla wewnętrzna szuka dowodu winy — jednego dzielnika. Gdy go nie znajdzie, flaga zostaje prawdziwa. To bardzo częsty wzorzec: „załóż, że tak, i szukaj kontrprzykładu”. |
d * d <= n |
Wystarczy sprawdzać dzielniki do pierwiastka z n. Gdyby n miało dzielnik większy od pierwiastka, to drugi czynnik byłby mniejszy — i znaleźlibyśmy go wcześniej. Zapis d * d <= n jest lepszy niż d <= Math.Sqrt(n): liczy na liczbach całkowitych, bez zaokrągleń i bez wywoływania metody w każdym obiegu. |
break po znalezieniu dzielnika |
Dla liczby 100 dzielnik znajdujemy przy d = 2. Bez break sprawdzalibyśmy jeszcze 3, 4, 5… aż do 10 — zupełnie bez sensu, bo odpowiedź już znamy. |
continue po nieudanym teście |
Alternatywą byłoby owinięcie trzech kolejnych instrukcji w if (pierwsza) { ... }. Wersja z continue jest płytsza i czyta się ją jak listę kroków: „nie jest pierwsza? następna”. |
Console.Write($"{n,4}") |
Write, nie WriteLine — wszystkie liczby jednego wiersza mają stać obok siebie. Szerokość 4 zapewnia równe kolumny także dla liczb dwucyfrowych. |
znalezione % 10 == 0 |
Łamanie wiersza co dziesiątą liczbę — po dziesiątej, dwudziestej i tak dalej. Sprawdzamy licznik znalezionych, a nie n, bo to znalezione trafiają do wiersza. |
| dwie liczby 2 i 3 | Warto prześledzić: dla n = 2 warunek 2 * 2 <= 2 jest od razu fałszywy, więc pętla wewnętrzna nie wykonuje się ani razu, flaga zostaje prawdziwa i dwójka trafia na listę. Dokładnie tak ma być — i to jest ta sytuacja, w której for wykonuje się zero razy. |
Ile obiegów oszczędza break
Dla granicy 50 różnica jest niezauważalna. Ale dla granicy miliona pętla wewnętrzna bez break wykonałaby się kilkaset razy dla każdej liczby parzystej, zamiast raz. To już różnica między sekundą a kilkoma minutami.
Na czym się najczęściej potykamy
| Błąd | Co się dzieje i jak poprawić |
|---|---|
break w switch ma zakończyć pętlę |
Kończy tylko switch. Użyj flagi sprawdzanej przez pętlę. |
break ma wyjść z dwóch pętli |
Wychodzi z najbliższej. Potrzebna flaga w warunku pętli zewnętrznej albo wydzielenie kodu do metody. |
continue przed i++ w while |
Pętla nieskończona — licznik nigdy nie rośnie. W while unikaj continue albo zwiększaj licznik na początku bloku. |
WriteLine w pętli wewnętrznej tabeli |
Każda wartość w osobnej linii. Wewnątrz Write, a WriteLine dopiero po pętli wewnętrznej. |
| ta sama nazwa licznika w obu pętlach | CS0136 — nie można zadeklarować i wewnątrz zasięgu, w którym i już istnieje. Używaj i, j, k albo nazw znaczących (wiersz, kolumna). |
kod po break w tym samym bloku |
CS0162: Unreachable code detected — ostrzeżenie o instrukcjach, do których program nigdy nie dojdzie. Zwykle oznacza, że break stoi za wcześnie. |
| zerowanie flagi w złym miejscu | bool pierwsza = true; musi być wewnątrz pętli zewnętrznej — flaga dotyczy jednej liczby. Postawiona przed pętlą zostałaby fałszywa po pierwszej liczbie złożonej i wszystkie kolejne uznano by za złożone. |
| niedoszacowanie liczby obiegów | Dwie pętle po 10 000 obiegów to sto milionów wykonań bloku wewnętrznego. Zanim uruchomisz — pomnóż zakresy. |
d <= n / 2 zamiast d * d <= n |
Działa poprawnie, ale robi znacznie więcej pracy. Dla liczby miliona: 500 000 sprawdzeń zamiast 1000. |
Zadania
Użytkownik podaje liczby. Sumuj je, ale gdy poda liczbę ujemną — przerwij i wypisz sumę bez niej. Maksymalnie dziesięć liczb.
Wypisz liczby od 1 do 30, pomijając te podzielne przez 3 lub przez 5. Na końcu podaj, ile liczb zostało wypisanych i ile pominiętych.
Wczytaj liczbę i poszukaj jej w tabliczce mnożenia od 1 do 20, czyli pierwszej pary i, j, dla której i * j równa się tej liczbie. Użyj dwóch pętli zagnieżdżonych. Po znalezieniu pary przerwij obie pętle — break wychodzi tylko z jednej, więc potrzebna jest zmienna logiczna albo warunek w pętli zewnętrznej; zapisz w komentarzu, który sposób wybrałeś. Pomijaj instrukcją continue wiersze, w których i jest większe od szukanej liczby, bo tam pary już nie będzie. Na końcu wypisz znalezioną parę albo komunikat o jej braku oraz liczbę wykonanych obiegów najgłębszego bloku. Sprawdź program dla liczb 56, 97 i 400.
Napisz program z menu tekstowym, które wyświetla cztery opcje: 1 — dodaj dwie liczby, 2 — odejmij dwie liczby, 3 — pokaż liczbę wykonanych operacji, 0 — koniec.
Po każdej operacji program wraca do menu i czeka na kolejny wybór; kończy się dopiero po wpisaniu zera. Nieznana opcja ma dawać komunikat i powrót do menu, a nie przerywać program. Na wyjściu wypisz łączną liczbę wykonanych operacji.
Do wyjścia z pętli użyj break, a do pominięcia reszty obiegu przy błędnym wyborze — continue.
Znajdź wszystkie trójki liczb całkowitych a ≤ b ≤ c z zakresu 1–30, dla których a² + b² = c². Policz, ile ich jest i ile razy wykonał się najgłębszy blok.
Wczytaj liczbę całkowitą większą od 1 i rozłóż ją na czynniki pierwsze, wypisując je w postaci iloczynu, np. 360 = 2 * 2 * 2 * 3 * 3 * 5.
Co trzeba zapamiętać
- break kończy najbliższy switch albo pętlę; continue przechodzi do następnego obiegu najbliższej pętli.
- continue w for przechodzi przez część kroku pętli. Ten krok nie musi zwiększać licznika.
- continue w while może pominąć zmianę warunku, jeśli umieszczono ją później w ciele.
- Wyjście z kilku poziomów można zorganizować flagą albo wydzieleniem pracy do metody z return.
- Iloczyn liczby obiegów dotyczy niezależnych zakresów pętli. Jeśli zakres wewnętrzny zależy od zewnętrznego, policz obiegi osobno.
- Przy sprawdzaniu pierwszości wystarczą dzielniki do pierwiastka. Dla dużych int zamiast d * d <= n można użyć d <= n / d (dla d > 0), aby uniknąć przepełnienia.
- Liczba break sama nie przesądza o jakości programu. Najważniejsza jest czytelność przepływu.