Вложенные циклы и типичные ошибки
Тренер ставит задачу: три подхода, в каждом — по три круга. Бегун пробегает три круга первого подхода, потом три круга второго, потом третьего. Всего получается три на три, то есть девять кругов.
Цикл внутри другого цикла называют вложенным. Внутренний цикл проходит все свои витки на каждом витке внешнего. Так удобно строить таблицы — например, таблицу умножения:
<?php
for ($row = 1; $row <= 3; $row++) {
$line = '';
for ($col = 1; $col <= 3; $col++) {
$line .= $row * $col . ' ';
}
echo $line, "\n";
}
// 1 2 3
// 2 4 6
// 3 6 9Давайте разберём, в каком порядке работают циклы:
- Внешний цикл перебирает ряды:
$rowот1до3. - В начале каждого ряда
$line = ''заводит пустую строку. - Внутренний цикл перебирает столбцы:
$colот1до3. На каждом витке он дописывает в$lineпроизведение и пробел. - Когда внутренний цикл закончился,
echoпечатает готовый ряд, а внешний цикл переходит к следующему.
Тело внутреннего цикла здесь выполняется девять раз. На собеседовании часто спрашивают, сколько раз выполнится тело вложенного цикла: число витков внешнего умножают на число витков внутреннего.
Здесь картинка с тренировкой неточна: бегун устаёт, а цикл нет, и вложенность умножает работу незаметно. Два цикла по тысяче витков — это уже миллион выполнений тела.
Ряд в одной строке
Ряд собирается в переменной $line по кусочкам. Как вы уже знаете, оператор .= дописывает строку в конец: после первого столбца в $line лежит '1 ', после второго — '1 2 '.
Важно, где стоит $line = '': внутри внешнего цикла, перед внутренним. Так каждый ряд начинается с пустой строки. Если поставить $line = '' перед внешним циклом, ряды склеятся и второй echo напечатает 1 2 3 2 4 6.
Собирать ряд в строку удобнее, чем печатать каждое число отдельным echo. Перевод строки "\n" нужен только один раз, в конце ряда.
Выход из двух циклов
break во внутреннем цикле завершает только его, и внешний цикл переходит к следующему ряду. Чтобы выйти сразу из обоих, пишут break 2: число говорит, из скольких вложенных циклов выйти. Этот код ищет в таблице первую клетку со значением 6:
for ($row = 1; $row <= 3; $row++) {
for ($col = 1; $col <= 3; $col++) {
if ($row * $col === 6) {
break 2;
}
}
}
echo $row, ' ', $col, "\n"; // 2 3Разберём по шагам:
- В первом ряду значения
1,2и3, шестёрки нет. - Во втором ряду при
$col, равном3, произведение равно6, иbreak 2завершает оба цикла. - Шаги
$col++и$row++уже не выполняются, поэтомуechoпечатает найденную клетку:2 3.
С обычным break внешний цикл продолжил бы поиск. Он нашёл бы ещё 3 * 2, закончился бы с $row, равным 4, и программа напечатала бы 4 2.
Забытый шаг счётчика
Во вложенных циклах два счётчика, и шаг иногда пишут не тому счётчику. В заголовке for ($col = 1; $col <= 3; $row++) шаг меняет $row вместо $col. Тогда $col навсегда остаётся 1, и внутренний цикл не заканчивается.
В таблице умножения такая программа долго не проработает: тело без конца дописывает в $line, и строка растёт, пока не займёт всю память, которую PHP отводит программе. Тогда PHP останавливает программу сообщением Fatal error: Allowed memory size of 134217728 bytes exhausted. Проверяйте заголовок: во всех трёх частях for стоит одно и то же имя.
Частая ошибка
Во внутреннем цикле написали < вместо <=:
for ($row = 1; $row <= 3; $row++) {
$line = '';
for ($col = 1; $col < 3; $col++) { // ✗
$line .= $row * $col . ' ';
}
echo $line, "\n";
}Программа работает без сообщений, но в каждом ряду два числа вместо трёх: 1 2, 2 4 и 3 6. Условие $col < 3 ложно уже при $col, равном 3, и третий столбец пропадает. Такую ошибку называют ошибкой на единицу: цикл делает на один виток больше или меньше, чем нужно.
Проверить границы помогают первый и последний виток:
$i = 1; $i <= $nпроходит$nвитков, от1до$n;$i = 0; $i < $nтоже проходит$nвитков, от0до$n - 1: так обходят индексы списка.
Главное
- Внутренний цикл проходит все свои витки на каждом витке внешнего; чтобы узнать, сколько витков всего, число витков внешнего умножают на число витков внутреннего.
- Ряд удобно собирать в строку через
.=и печатать один раз, а каждый ряд начинать с$line = ''. breakвыходит только из внутреннего цикла,break 2— сразу из двух.<и<=отличаются на один виток: проверяйте первый и последний.