Základní kurz 11: Cykly

Autor: Joker
Vedle jednoduchého provádění příkazů v řadě za sebou a podmínek, o kterých jsme mluvili v minulé kapitole, jsou cykly (nebo také iterace) třetí základní struktura pro řízení běhu programu. Zatímco základní myšlenka u podmínek by šla vyjádřit jako „Proveď daný blok kódu jen tehdy, jestliže je splněna nějaká podmínka“ (stejná myšlenka jde použít i pro blok else, pokud jako podmínku vezmeme nesplnění podmínky zapsané v if), základní myšlenka u cyklů je „Opakuj daný blok kódu tak dlouho, dokud je splněna nějaká podmínka“.

Cyklus foreach

Na jeden druh situací, kdy cykly jdou využít, jste asi narazili už když jsme mluvili o polích: Jak provést nějakou operaci pro každý prvek pole? Ve smyslu výše uvedené myšlenky by to tedy bylo: „Opakuj načtení dalšího
prvku pole a nějakou operaci s ním tak dlouho, dokud ten další prvek existuje“. K tomu slouží v PHP foreach. Použití je:

foreach ($pole as $klic => $hodnota) {
// kód
}
Tento cyklus projde pole $pole, pro každou položku její klíč v poli přiřadí do proměnné $klic, hodnotu do proměnné $hodnota a provede kód v bloku. Takto se projdou jen hodnoty položek a postupně přiřadí do proměnné $hodnota.
Příklad:
<?php
$pole = array("a", "b", "c", "d", "e");
foreach ($pole as $index => $hodnota) {
  echo "V poli na indexu ".$index." je hodnota: ".$hodnota."<br>";
}
?>

Pokud klíče položek v poli pro daný cyklus nepotřebujete, je možné zápis zkrátit:
<?php
$pole = array("a", "b", "c", "d", "e");
echo "Pole obsahuje hodnoty: ";
foreach ($pole as $hodnota) {
  echo $hodnota."<br>";
}
?>

Některé vlastnosti cyklu foreach, které nemusejí být na první pohled zřejmé:

Cyklus while

Další druhy cyklů se už dají použít k více účelům, než jen procházení polí. Nejjednodušší z nich je while.
Zápis i princip se podobá podmínce if:

while (podmínka) {
// kód
}
Při každém cyklu se nejdříve vyhodnotí podmínka jako logická hodnota. Je-li výsledek false, cyklus končí a pokračuje se dalším příkazem za ním. Je-li výsledek true, vykoná se kód v bloku while a pokračuje se dalším cyklem, tedy se znovu vyhodnotí podmínka, a tak dále.

Z uvedeného principu vyplývá jedna důležitá věc: Podmínka musí záviset na něčem, co se během průchodu cyklem změní (nebo alespoň může změnit). Pokud je podmínka pořád stejná, cyklus nikdy neskončí. Někdy lze vidět úmyslné konstrukce typu while(true), kdy autor kódu pak vyskočí z cyklu někde uvnitř (o tom si povíme níže), ale v drtivé většině případů je použití takové konstrukce známkou špatného návrhu kódu, takže je lepší se tomu vyhýbat.

Časté použití cyklu while je pro načítání řádků ze souboru nebo záznamů z databáze, což si ukážeme v pokročilejších kapitolách zde na webu. Jako příklad můžeme uvést cyklus, který spočítá sumu čísel od 1 do určeného čísla:

<?php
$cislo = 10;
$soucet = 0;
// Kopie původního čísla pro výpis na konci
$puvodni = $cislo;
// Dokud číslo je větší než 0
while ($cislo > 0) {
  // přičteme číslo k součtu
  $soucet = $soucet + $cislo;
  // a snížíme číslo o 1
  $cislo = $cislo - 1;
}
echo "Suma čísel od 1 do $puvodni je: $soucet";
?>

Cyklus do … while

Je velmi podobný cyklu while, liší se jen tím, že zatímco cyklus while vždy nejdřív vyhodnotí podmínku a pak (je-li splněna) provede kód, cyklus do-while vždy nejdřív provede kód a pak vyhodnotí podmínku (a je-li splněna, pokračuje dalším cyklem). Z toho vyplývá, že cyklus do-while se vždy provede alespoň jednou, i když podmínka už od začátku není splněna.
Předchozí příklad přepsaný na cyklus do-while by vypadal takto:

<?php
$cislo = 10;
$soucet = 0;
$puvodni = $cislo;
// Podmínka je uvedena až na konci cyklu
do {
  // přičteme číslo k součtu
  $soucet = $soucet + $cislo;
  // a snížíme číslo o 1
  $cislo = $cislo - 1;
  // Jelikož snižujeme hodnotu už před podmínkou, je v podmínce 1 a ne 0
} while ($cislo > 1);
echo "Suma čísel od 1 do $puvodni je: $soucet";
?>

Obvykle je výhodnější použít cyklus while, než do-while (i ve výše uvedeném příkladu, protože vyhodnocení podmínky před cyklem odfiltruje různé neplatné hodnoty, které by v $cislo mohly na začátku být), ale v některých situacích se do-while hodí. Příklad takové situace může být operace typu „Čti další záznam (ze souboru/databáze) tak dlouho, dokud jeho hodnota nesplňuje nějakou podmínku“, protože tam nejméně jeden záznam chceme načíst vždy.

Cyklus for

Posledním typem cyklu je for. Hodí se pro specifickou, ale velmi častou, situaci, kdy cyklus spočívá v tom, že provedeme kód kde zvyšujeme (případně snižujeme) hodnotu nějaké proměnné (počítadla) a to opakujeme tak dlouho, než proměnná dosáhne nějaké hodnoty. Tento typ situace je i náš ukázkový příklad (v kódu snižujeme hodnotu $cislo tak dlouho, až není vyšší než 0, respektive 1). Cyklus for je na začátku složitější na pochopení, ale pak tuto situaci naopak zpřehledňuje a zkracuje zápis. Může vypadat asi takto:

for ($i = 0; $i < 10; $i = $i+1) {
// kód
}
Vidíme, že zatímco například u cyklu while byla v závorce jen podmínka, zde se závorka dělí na tři části. První z nich (v příkladu tedy $i = 0;) je příkaz, který se provede jednou před prvním cyklem. Obvykle, stejně jako v uvedeném příkladu, se tato část používá k inicializaci proměnné s počítadlem. Druhá část (v příkladu výše $i < 10) je podmínka a funguje stejně, jako u cyklu while: Vyhodnotí se na začátku každého cyklu a kód se provede jen tehdy, když výsledná hodnota odpovídá logické hodnotě true. Třetí část (v příkladu $i = $i+1) se provede vždy na konci každého cyklu (po vykonání kódu) a jejím smyslem je změnit počítadlo. Výše uvedený příklad je tedy funkčně stejný jako tento cyklus while:
$i = 0;
while ($i < 10) {
// kód
$i = $i + 1;
}
Ovšem cyklus for je kratší a přehlednější (protože práce s počítadlem je pohromadě na jednom řádku). Příklad, na kterém jsme demonstrovali předchozí dva typy cyklů, by šel napsat i pomocí cyklu for:
<?php
$vstup = 10;
$soucet = 0;
for ($cislo = $vstup; $cislo > 0; $cislo = $cislo - 1) {
  $soucet = $soucet + $cislo;
}
echo "Suma čísel od 1 do $vstup je: $soucet";
?>

Pro lepší přehlednost rozepisujeme zvyšování či snižování čísla o 1 výrazem např. $cislo = $cislo - 1;. V praxi se používá operátor inkrementace či dekrementace ($cislo++;, resp. $cislo--;).

Vnořené cykly

Nic nám nebrání umístit do bloku kódu vykonávaného v cyklu další cyklus a vytvořit tak vnořený cyklus. Jako příklad si můžeme uvést vypisování tabulek, kdy jeden cyklus vypisuje řádky a vnořený cyklus vypisuje sloupce v řádku.
Příklad:

<?php
  // vnější cyklus pro řádky
  for ($r = 1; $r < 5; $r++) {
    // vnitřní cyklus pro sloupce
    for ($s = 1; $s < 5; $s++) {
      echo "[$r;$s] "; // vypsání hodnoty buňky ve vnitřním cyklu
    }
    echo "<br>"; // ukončení řádku ve vnějším cyklu
  }
?>

Přerušení cyklu

Může se stát, že v určité situaci chcete z cyklu předčasně vyskočit. Nemělo by se to ale stávat často, obvykle je možné se skokům vyhnout lepším návrhem kódu a měla by to být preferovaná varianta. Přesto se výskok z cyklu může v některých případech hodit.

continue

Výskok z cyklu může mít dvě podoby: Buď chcete ukončit aktuální cyklus a pokračovat dalším cyklem, nebo chcete ukončit cykus jako celek (tj. další cyklus už neprovádět). V první situaci (pokračování dalším cyklem) se používá příkaz continue. Například kdybychom chtěli vypsat všechny hodnoty nějakého pole, kromě nul:

<?php
$pole = array(1, 5, 0, 4, 9, 0, 3);
foreach ($pole as $klic => $hodnota) {
  // při nule se zbytek cyklu přeskočí
  if ($hodnota === 0) continue;

  echo "Hodnota na indexu $klic v poli je: $hodnota <br>";
}
?>
U vnořených cyklů lze použít continue n, kde n je číslo udávající, kolik vnořených cyklů se má ukončit. Tedy continue 1 je totéž jako continue a ukončí stávající cyklus, continue 2 ukončí stávající cyklus a cyklus o úroveň nad ním, a tak dále.

Příkaz continue lze kromě cyklů použít i pro výskok z větve podmínky switch a podmínka switch se počítá i jako úroveň vnoření cyklu.

Použití continue jde nahradit použitím podmínky a většinou je to i lepší řešení. Pro náš konkrétní příklad by kód šel napsat takto:

<?php
$pole = array(1, 5, 0, 4, 9, 0, 3);
foreach ($pole as $klic => $hodnota) {
  if ($hodnota !== 0){
    echo "Hodnota na indexu $klic v poli je: $hodnota <br>";
  }
}
?>

break

Druhý případ výskoku z cyklu je situace, kdy chceme předčasně ukončit cyklus jako celek. K tomu slouží příkaz break, který už známe jako ukončení bloku case z minulé kapitoly o podmínkách. Kromě bloku case tedy příkaz break (okamžitě) ukončí i cyklus a pokračuje dalším příkazem za ním. Stejně jako u příkazu continue lze použít break n pro vyskočení z více cyklů či bloků switch najednou, např. break 2 vyskočí ze stávajícího cyklu (nebo bloku switch) a ještě toho o úroveň výše, pokračuje se příkazem následujícím za vnějším cyklem (resp. blokem switch). Vyskakování z více úrovní najednou může skript velmi znepřehlednit, takže ho používejte jen výjimečně a věnujte pozornost čitelnosti skriptu.

Použití příkazu break na úrovni hlavního skriptu (tj. když nejste v žádném cyklu či bloku switch) ukončí celý skript. Na to pamatujte při vyskakování z více cyklů přes break n nebo kdybyste se snažili přes break ukončit například podmínku if (což nejde a pokud nejste zrovna uvnitř cyklu, ukončíte tím celý skript).

Pro ukázku můžeme opět vypisovat hodnoty z pole jako výše, ale tentokrát budeme chtít vypisovat hodnoty jen do chvíle, než narazíme na první nulu (tedy u námi definovaného pole se vypíší pouze první dvě hodnoty). Kód bude prakticky stejný,
jen místo příkazu continue použijeme break:

<?php
$pole = array(1, 5, 0, 4, 9, 0, 3);
foreach ($pole as $klic => $hodnota) {
  // Při nule cyklus skončí
  if ($hodnota === 0) break;

  echo "Hodnota na indexu $klic v poli je: $hodnota <br>";
}
?>
I příkazu break se lze vyhnout, ale v některých případech to může znamenat poměrně rozsáhlé přepracování kódu, zejména když jen upravujete nějaký už hotový kód. Pokud ale existují podobně náročné varianty s break a bez, je lepší preferovat variantu bez použití break.

Také příklad výše by šel přepsat bez použití break, ale vyžadovalo by to pozměnit celý návrh cyklu. Můžeme si ale při té příležitosti ukázat další způsob procházení pole:

<?php
$pole = array(1, 5, 0, 4, 9, 0, 3);
// funkce count() vrátí počet položek v poli
$pocet = count($pole);
$i = 0;
while (($i < $pocet) && ($pole[$i] !== 0)) {
  echo "Hodnota na indexu $i v poli je: $pole[$i] <br>";
  $i++;
}
?>

Příklad výše demonstruje, že podmínka nemusí být jen jednoduchá „pokud počítadlo nedosáhlo nějaké hodnoty“, ale může být zapsaná i výrazem. Také vidíme, že vynecháním druhé podmínky v příkladu výše by bylo možné procházet celé pole použitím cyklu while, ovšem i tak je k procházení celého pole jednodušší a přehlednější využít cyklus foreach.

Vyzkoušejte si

1. Napište skript, který vypíše lichá čísla od 10 do 50.

2. Vytvořte si pole s názvy dnů v týdnu a pak zkuste vypsat seznam dnů v týdnu, přičemž aktuální den v týdnu bude
zvýrazněný značkou <strong>.
Číslo aktuálního dne v týdnu zjistíte pomocí date("w"), pro české prostředí to ale má záludnost: Používá se
americké pořadí dnů v týdnu (tj. první den je neděle) a čísluje se od nuly. Tedy 0 = neděle, 1 = pondělí, atd.

Řešení (kromě uvedených existují i jiná možná řešení):
1.

<?php
// 10 a 50 jsou sudá čísla, takže je do cyklu nemusíme zahrnovat
for ($i = 11; $i < 50; $i++) {
  // Liché číslo se určí tak, že modulo (zbytek po dělení) 2 je 1
  if (($i % 2) == 1) {
    echo $i." ";
  }
}
?>

2.
<?php
  // vložení hlavičky stránky, kterou jsme vytvořili ve 4. kapitole
  include "spolecna_hlavicka.php";

  // Neděli vložíme poslední, aby ve foreach byla na konci, ale s indexem 0
  // Při array("neděle", "pondělí", …) by neděle byla první i ve výpisu
  $dny = array(1=>"pondělí", "úterý", "středa", "čtvrtek", "pátek", "sobota", 0=>"neděle");
  $dnes = date("w");

  foreach($dny as $i => $den) {
    if($i == $dnes) {
      echo "<strong>$den</strong> ";
    }
    else {
      echo $den." ";
    }
  }
?>

Poznámka:
Všimněte si, že v příkladech výše jsou jednotlivé položky oddělené mezerami, přičemž mezera se vypíše i za poslední
vypsanou položku. U mezery to není vidět, ale kdybychom chtěli položky oddělit třeba čárkami, vypadalo by to ošklivě.
Řešením je vypisovat oddělovač jen za podmínky, že daná položka není poslední. Alternativa je vypsat oddělovat před
položkou a vynechat první položku (obvykle je snadné zjistit, zda je položka první, ale může být těžké zjistit, zda je
poslední). Kdybychom takto upravili 1. příklad (výpis lichých čísel od 10 do 50), mohlo by to vypadat:
<?php
// Indikátor, zda je první položka
$prvni = true;
// 10 a 50 jsou sudá čísla, takže je do cyklu nemusíme zahrnovat
for ($i = 11; $i < 50; $i++) {
  // Liché číslo se určí tak, že modulo (zbytek po dělení) 2 je 1
  if (($i % 2) == 1) {
    // Pokud je první položka
    if ($prvni) {
      $prvni = false; // zruší se indikátor první položky
    }
    else {
      echo ", "; // jinak se vypíše oddělovač
    }
    echo $i;
  }
}
?>
První položku by šlo testovat i podle hodnoty, tedy v našem příkladu napsat podmínku na ($i == 11), ale to má nevýhodu, že počáteční hodnota cyklu se pak vyskytuje na dvou místech a kdyby se nějak měnila, bude nutné upravit obě.

Máte návrh na vylepšení či doplnění článku? Obsahuje článek nepřesné informace, nebo v něm chybí něco důležitého?
Tento článek má diskusní vlákno na diskusi Jak Psát Web, kam můžete náměty a připomínky napsat.


Správcem webu Péhápko.cz je Joker, mail zavináč it-joker tečka cz. Informace o autorských právech a možnostech použití obsahu viz Autorská práva
Přihlášení