Введение: Почему foreach — это не просто «для каждого»
Если вы пишете на PHP хотя бы неделю, вы уже сто раз использовали foreach. Это, пожалуй, самый любимый цикл каждого разработчика: коротко, элегантно, без лишних счетчиков. Но задумывались ли вы, что на самом деле происходит под капотом, когда вы пишете foreach ($array as $value)?
Вопрос «How does PHP 'foreach' actually work?» набрал тысячи просмотров на Stack Overflow. И это не случайно. Поведение foreach таит в себе несколько сюрпризов, особенно когда речь заходит о ссылках, вложенных циклах и нестандартных массивах.
В этом туториале мы не просто повторим синтаксис. Мы заглянем внутрь движка PHP, разберемся с внутренним указателем массива, увидим, как работают итераторы, и научимся избегать подводных камней. Поехали!
1. Анатомия foreach: что говорит документация
Начнем с базы. foreach предназначен для перебора массивов и объектов. Синтаксис прост:
<?php
$fruits = ['apple', 'banana', 'cherry'];
foreach ($fruits as $fruit) {
echo $fruit . "\n";
}
// Вывод: apple banana cherry
?>
Но есть и вторая форма — с ключом:
<?php
$prices = ['apple' => 100, 'banana' => 50];
foreach ($prices as $name => $price) {
echo "$name стоит $price руб.\n";
}
?>
Кажется, всё очевидно. Но давайте копнем глубже. PHP обрабатывает foreach не так, как for или while. Внутри он использует внутренний указатель массива (internal array pointer) и специальный механизм итерации.
2. Внутренний указатель массива: секретный агент
У каждого массива в PHP есть невидимый «курсор» — внутренний указатель. Он показывает на текущий элемент. Функции current(), next(), prev(), end() и reset() управляют именно им.
Когда вы запускаете foreach, PHP делает следующее:
- Сбрасывает внутренний указатель на первый элемент (как
reset()). - На каждой итерации берет элемент, на который указывает указатель.
- Сдвигает указатель на следующий элемент (как
next()). - Повторяет, пока не дойдет до конца.
Посмотрим на это в действии:
<?php
$array = [10, 20, 30];
// Ручная имитация foreach
reset($array); // Указатель на первый элемент
while ($value = current($array)) {
echo $value . " ";
next($array); // Сдвигаем указатель
}
// Вывод: 10 20 30
?>
Но есть нюанс: если массив пуст или содержит false, этот трюк сломается. foreach же работает корректно всегда, потому что использует более надежный внутренний механизм — итераторы.
3. Итераторы и foreach: современный подход
Начиная с PHP 5, foreach может работать не только с массивами, но и с любыми объектами, реализующими интерфейс Traversable (или его наследника Iterator).
Когда вы передаете объект в foreach, PHP вызывает методы итератора:
rewind()— сброс в началоvalid()— проверка, есть ли элементcurrent()— получить значениеkey()— получить ключnext()— перейти к следующему
Пример пользовательского итератора:
<?php
class NumbersIterator implements Iterator {
private $position = 0;
private $numbers = [1, 2, 3, 4, 5];
public function rewind() {
$this->position = 0;
}
public function current() {
return $this->numbers[$this->position];
}
public function key() {
return $this->position;
}
public function next() {
++$this->position;
}
public function valid() {
return isset($this->numbers[$this->position]);
}
}
$iterator = new NumbersIterator();
foreach ($iterator as $key => $value) {
echo "$key: $value\n";
}
// Вывод: 0: 1, 1: 2, 2: 3, 3: 4, 4: 5
?>
Это дает невероятную гибкость: вы можете перебирать файлы, строки базы данных, результаты API — всё, что угодно, если это упаковано в итератор.
4. Ссылки в foreach: мощный инструмент и источник багов
Один из самых коварных моментов — использование & (ссылки) в foreach. Рассмотрим пример:
<?php
$numbers = [1, 2, 3, 4];
// Перебор по ссылке
foreach ($numbers as &$number) {
$number *= 2;
}
unset($number); // ВАЖНО! Удаляем ссылку
print_r($numbers);
// Вывод: [2, 4, 6, 8]
?>
Всё работает как часы. Но что произойдет, если не удалить ссылку после цикла? Переменная $number останется ссылкой на последний элемент массива. Если вы потом измените $number, вы измените и массив!
Классический баг:
<?php
$array = [1, 2, 3];
foreach ($array as &$value) {
// ничего не делаем
}
// Забыли unset($value)
foreach ($array as $value) {
// Теперь $value — это ссылка на последний элемент!
// На каждой итерации мы меняем последний элемент массива
}
print_r($array);
// Вывод: [1, 2, 2] — третий элемент стал равен второму!
?>
Золотое правило: всегда используйте unset($var) после цикла foreach со ссылкой. Или, еще лучше, избегайте ссылок, если это не критично.
5. Вложенные foreach: как не запутаться
Когда вы вкладываете один foreach в другой, внутренний указатель массива может вести себя неожиданно. PHP 7+ справляется с этим лучше, но в старых версиях были проблемы.
Пример безопасного вложенного цикла:
<?php
$matrix = [
[1, 2],
[3, 4],
[5, 6]
];
foreach ($matrix as $row) {
foreach ($row as $cell) {
echo $cell . " ";
}
echo "\n";
}
// Вывод: