Iteradores y generadores en PHP: guía detallada
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы — библиотекарь, и перед вами стоит огромный стеллаж с тысячами книг. Вам нужно прочитать названия всех книг, чтобы составить каталог. У вас есть два способа это сделать:
Способ 1 (без итератора): Вы снимаете все книги с полок, складываете их в огромную тележку, везёте к столу и только потом начинаете читать названия. Проблема: тележка может не вместить все книги, а если книг миллион — вы просто не сможете их все одновременно удержать в голове или в тележке.
Способ 2 (с итератором): Вы берёте блокнот и ручку, подходите к стеллажу, читаете первую книгу, записываете, переходите ко второй, третьей... Вы не тащите все книги сразу — вы обрабатываете их по одной, по порядку. И в любой момент вы можете остановиться, отдохнуть, а потом продолжить с того же места.
В программировании происходит то же самое. У нас есть коллекции данных: массивы, списки, результаты из базы данных, строки из файла. Иногда эти коллекции огромны — миллионы записей. Загружать их все в память сразу — это как пытаться уместить океан в стакан. Компьютер просто "захлебнётся" и упадёт.
Итераторы — это те самые "библиотекари с блокнотом". Они позволяют проходить по элементам коллекции по одному, не загружая всё сразу. Вы просто говорите: "Дай следующий элемент", и итератор его выдаёт. Когда элементы закончатся — он скажет "Стоп, больше нет".
Генераторы — это ещё более крутая штука. Представьте, что вы не просто читаете готовые книги, а сами пишете их на ходу. Генератор — это функция, которая может "заморозиться" на полуслове, выдать результат, а потом, когда её попросят, продолжить с того же места. Это как сериал: вы посмотрели серию, нажали паузу, а через неделю нажали Play и продолжили с того же момента.
Важно: Итераторы и генераторы экономят память. Если у вас есть файл на 10 гигабайт, вы можете читать его построчно с помощью генератора, и в памяти будет только одна строка, а не весь файл. Без генератора программа бы просто "упала" от нехватки памяти.
Теория: как это работает изнутри
Давайте заглянем "под капот" и разберёмся, как устроены итераторы и генераторы в PHP. Не пугайтесь — всё логично.
Итераторы: интерфейс и договорённости
В PHP есть специальный "контракт" (интерфейс) под названием Iterator. Любой класс, который хочет стать итератором, должен выполнить этот контракт — то есть реализовать пять обязательных методов. Это как правила дорожного движения: если ты водитель, ты обязан знать и выполнять эти правила.
Вот эти пять методов:
| Метод | Что делает | Аналогия из жизни |
|---|---|---|
current() |
Возвращает текущий элемент | Вы смотрите на книгу, которая сейчас перед вами |
key() |
Возвращает ключ (номер или имя) текущего элемента | Вы смотрите на номер страницы, на которой остановились |
next() |
Переходит к следующему элементу | Вы переворачиваете страницу |
rewind() |
Сбрасывает итератор в начало | Вы закрываете книгу и открываете её снова на первой странице |
valid() |
Проверяет, есть ли ещё элементы | Вы смотрите, не закончилась ли книга |
Когда вы пишете foreach ($collection as $key => $value), PHP внутри делает примерно так:
// PHP делает это за кулисами, когда вы используете foreach
$iterator->rewind(); // Встаём в начало
while ($iterator->valid()) { // Пока есть элементы
$key = $iterator->key(); // Берём ключ
$value = $iterator->current(); // Берём значение
// ... ваш код ...
$iterator->next(); // Переходим к следующему
}
Генераторы: функции с "паузой"
Генератор — это особая функция, в которой есть ключевое слово yield. Когда PHP встречает yield, он:
- Запоминает всё состояние функции (все переменные, место в коде)
- Возвращает значение наружу
- Засыпает и ждёт, пока его не попросят продолжить
Когда вы снова вызываете next() у генератора, он просыпается и продолжает выполнение с того самого места, где остановился. Это называется "ленивые вычисления" — значение вычисляется только тогда, когда оно реально нужно.
Ключевое отличие: Обычная функция выполняется от начала до конца и возвращает одно значение. Генератор может "выдавать" много значений по одному, засыпая между ними. Обычная функция — это курьер, который привёз одну посылку. Генератор — это конвейер, который выдаёт детали одну за другой.
Синтаксис в PHP: все формы и варианты
В PHP есть несколько способов работать с итераторами и генераторами. Давайте разберём каждый.
1. Встроенные итераторы (SPL — Standard PHP Library)
PHP уже имеет готовые итераторы для самых частых задач. Вам не нужно писать свои — просто берите и пользуйтесь.
| Итератор | Для чего | Пример использования |
|---|---|---|
ArrayIterator |
Превращает обычный массив в итератор | Когда нужно передать массив туда, где ожидается итератор |
DirectoryIterator |
Обходит файлы и папки в директории | Список всех файлов в папке |
FilesystemIterator |
То же, но быстрее и проще | Обход файлов без лишней информации |
LimitIterator |
Ограничивает количество элементов | Взять первые 10 записей из 1000 |
FilterIterator |
Пропускает только подходящие элементы | Оставить только файлы с расширением .php |
2. Создание своего итератора через класс
Если готовых итераторов недостаточно, вы можете создать свой. Для этого нужно реализовать интерфейс Iterator.
// Создаём свой итератор для обхода чисел Фибоначчи
class FibonacciIterator implements Iterator {
private int $previous = 0; // Предыдущее число
private int $current = 1; // Текущее число
private int $position = 0; // Позиция (ключ)
private int $maxCount; // Сколько чисел хотим получить
public function __construct(int $maxCount) {
$this->maxCount = $maxCount;
}
// Возвращает текущее значение (число Фибоначчи)
public function current(): mixed {
return $this->current;
}
// Возвращает ключ (номер числа по порядку)
public function key(): mixed {
return $this->position;
}
// Переход к следующему числу
public function next(): void {
// Вычисляем следующее число Фибоначчи
$next = $this->previous + $this->current;
$this->previous = $this->current;
$this->current = $next;
$this->position++;
}
// Сброс в начало
public function rewind(): void {
$this->previous = 0;
$this->current = 1;
$this->position = 0;
}
// Проверка: есть ли ещё элементы?
public function valid(): bool {
return $this->position < $this->maxCount;
}
}
// Используем наш итератор
$fib = new FibonacciIterator(10);
foreach ($fib as $key => $value) {
echo "Число #$key: $value
";
}
// Выведет: Число #0: 1, Число #1: 1, Число #2: 2, Число #3: 3, ...
3. Генераторы — самый простой способ
Генераторы — это "ленивый" вариант итератора. Вам не нужно писать целый класс с пятью методами. Достаточно написать функцию с yield.
// Генератор чисел Фибоначчи (та же задача, но в 5 строк)
function fibonacciGenerator(int $maxCount): Generator {
$previous = 0;
$current = 1;
for ($i = 0; $i < $maxCount; $i++) {
yield $i => $current; // Отдаём значение и засыпаем
// Когда попросят — просыпаемся и продолжаем
$next = $previous + $current;
$previous = $current;
$current = $next;
}
}
// Используем генератор
foreach (fibonacciGenerator(10) as $key => $value) {
echo "Число #$key: $value
";
}
4. Генераторы с ключами и без
Вы можете отдавать только значения (без ключей) или указывать ключи явно:
// Без ключей — ключи будут 0, 1, 2...
function simpleGenerator(): Generator {
yield 'первый';
yield 'второй';
yield 'третий';
}
// С ключами
function keyedGenerator(): Generator {
yield 'name' => 'Иван';
yield 'age' => 25;
yield 'city' => 'Москва';
}
// yield без значения — вернёт null
function nullGenerator(): Generator {
yield; // current() вернёт null
yield 'теперь есть значение';
}
5. Генераторы с делегированием (yield from)
Можно вложить один генератор в другой с помощью yield from. Это как матрёшка:
function generatorA(): Generator {
yield 'A1';
yield 'A2';
}
function generatorB(): Generator {
yield 'B1';
yield from generatorA(); // Вставляем все значения из A
yield 'B2';
}
foreach (generatorB() as $value) {
echo "$value "; // B1 A1 A2 B2
}
6. Генераторы как массивы (с помощью iterator_to_array)
Если вам всё-таки нужен обычный массив, можно "развернуть" генератор:
function rangeGenerator(int $start, int $end): Generator {
for ($i = $start; $i <= $end; $i++) {
yield $i;
}
}
// Превращаем генератор в массив
$numbers = iterator_to_array(rangeGenerator(1, 5));
print_r($numbers); // [1, 2, 3, 4, 5]
Предупреждение: Используйте iterator_to_array только если уверены, что данных мало. Иначе вы теряете весь смысл генератора — экономию памяти.
Примеры кода — от простого к сложному
Пример 1: Чтение большого файла построчно
Это классика. Файл на 1 ГБ, но в памяти только одна строка.
// Функция-генератор для чтения файла
function readFileLines(string $filename): Generator {
// Открываем файл
$handle = fopen($filename, 'r');
// Если файл не открылся — ошибка
if (!$handle) {
throw new Exception("Не могу открыть файл: $filename");
}
// Читаем строку за строкой
while (($line = fgets($handle)) !== false) {
// Отдаём текущую строку (убираем пробелы по краям)
yield trim($line);
}
// Закрываем файл
fclose($handle);
}
// Используем генератор
foreach (readFileLines('big_log.txt') as $lineNumber => $line) {
// Обрабатываем только строки, содержащие "ERROR"
if (str_contains($line, 'ERROR')) {
echo "Строка $lineNumber: $line
";
}
// Память занята только одной строкой, а не всем файлом!
}
Пример 2: Бесконечный генератор (числа Фибоначчи)
Генератор может работать бесконечно — он не хранит все значения.
// Бесконечный генератор чисел Фибоначчи
function infiniteFibonacci(): Generator {
$a = 0;
$b = 1;
while (true) { // Бесконечный цикл!
yield $b;
[$a, $b] = [$b, $a + $b]; // Обмен значениями (короткая запись)
}
}
// Берём первые 20 чисел
$count = 0;
foreach (infiniteFibonacci() as $number) {
echo "$number ";
$count++;
if ($count >= 20) break; // Останавливаемся сами
}
// Выведет: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
Пример 3: Генератор с фильтрацией
Генератор может не только отдавать данные, но и фильтровать их на лету.
// Генератор простых чисел (до N)
function primeGenerator(int $limit): Generator {
for ($number = 2; $number <= $limit; $number++) {
$isPrime = true;
// Проверяем, делится ли число на что-то кроме 1 и себя
for ($divisor = 2; $divisor <= sqrt($number); $divisor++) {
if ($number % $divisor == 0) {
$isPrime = false;
break;
}
}
// Если число простое — отдаём его
if ($isPrime) {
yield $number;
}
}
}
// Выводим все простые числа до 50
foreach (primeGenerator(50) as $prime) {
echo "$prime "; // 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
}
Пример 4: Свой итератор для обхода дерева (папки)
Представьте, что у вас есть вложенные папки. Нужно найти все файлы с расширением .php.
// Итератор для рекурсивного обхода папок
class PhpFileIterator implements Iterator {
private array $files = []; // Все найденные PHP-файлы
private int $position = 0; // Текущая позиция
public function __construct(string $rootDir) {
$this->findPhpFiles($rootDir);
}
// Рекурсивно ищем PHP-файлы
private function findPhpFiles(string $dir): void {
$items = scandir($dir);
foreach ($items as $item) {
// Пропускаем текущую и родительскую папку
if ($item == '.' || $item == '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) {
// Если это папка — заходим в неё рекурсивно
$this->findPhpFiles($path);
} elseif (pathinfo($path, PATHINFO_EXTENSION) == 'php') {
// Если это PHP-файл — добавляем в список
$this->files[] = $path;
}
}
}
public function current(): mixed {
return $this->files[$this->position];
}
public function key(): mixed {
return $this->position;
}
public function next(): void {
$this->position++;
}
public function rewind(): void {
$this->position = 0;
}
public function valid(): bool {
return isset($this->files[$this->position]);
}
}
// Используем
$iterator = new PhpFileIterator('/var/www/project');
foreach ($iterator as $file) {
echo "Найден PHP-файл: $file
";
}
Пример 5: Генератор с отправкой данных (send)
Генератор может не только отдавать, но и принимать данные. Это двухсторонняя связь!
// Генератор-калькулятор: принимает числа и возвращает сумму
function accumulator(): Generator {
$sum = 0;
// Бесконечно принимаем числа
while (true) {
// yield получает значение из send() и отдаёт текущую сумму
$number = yield $sum;
if ($number === null) {
// Если ничего не прислали — выходим
break;
}
$sum += $number;
}
return $sum; // Финальный результат
}
// Используем
$gen = accumulator();
// Первый вызов current() — запускаем генератор до первого yield
echo $gen->current() . "
"; // 0 (начальная сумма)
// Отправляем числа через send()
echo $gen->send(10) . "
"; // 10 (0 + 10)
echo $gen->send(20) . "
"; // 30 (10 + 20)
echo $gen->send(30) . "
"; // 60 (30 + 30)
// Завершаем генератор
$gen->send(null);
echo "Финальная сумма: " . $gen->getReturn() . "
"; // 60
Пример 6: Комбинирование нескольких генераторов
Можно объединять данные из разных источников.
// Генератор чётных чисел
function evenNumbers(int $limit): Generator {
for ($i = 2; $i <= $limit; $i += 2) {
yield $i;
}
}
// Генератор нечётных чисел
function oddNumbers(int $limit): Generator {
for ($i = 1; $i <= $limit; $i += 2) {
yield $i;
}
}
// Генератор, который чередует чётные и нечётные
function interleave(int $limit): Generator {
$evens = evenNumbers($limit);
$odds = oddNumbers($limit);
// Пока есть хоть один источник — чередуем
while ($evens->valid() || $odds->valid()) {
if ($evens->valid()) {
yield 'even' => $evens->current();
$evens->next();
}
if ($odds->valid()) {
yield 'odd' => $odds->current();
$odds->next();
}
}
}
foreach (interleave(10) as $type => $number) {
echo "$type: $number
";
}
// even: 2
// odd: 1
// even: 4
// odd: 3
// ...
Пример 7: Генератор для пагинации (API с постраничной выдачей)
Реальная задача: нужно получить все страницы из API, но мы не знаем, сколько их.
// Генератор для постраничного получения данных из API
function apiPaginator(string $url, int $perPage = 100): Generator {
$page = 1;
$hasMore = true;
while ($hasMore) {
// Формируем URL с параметрами страницы
$requestUrl = $url . '?page=' . $page . '&per_page=' . $perPage;
// Имитируем запрос к API (в реальности — curl или file_get_contents)
$response = file_get_contents($requestUrl);
$data = json_decode($response, true);
// Если данных нет — заканчиваем
if (empty($data['items'])) {
$hasMore = false;
break;
}
// Отдаём каждый элемент со страницы
foreach ($data['items'] as $item) {
yield $item;
}
// Проверяем, есть ли следующая страница
$hasMore = !empty($data['next_page']);
$page++;
}
}
// Используем: обрабатываем элементы по одному, не загружая всё сразу
foreach (apiPaginator('https://api.example.com/users') as $user) {
echo "Обрабатываем пользователя: " . $user['name'] . "
";
// Здесь можно сохранить в БД, отправить письмо и т.д.
}
Сравнение с Python: чем похоже и чем отличается
Если вы изучали Python, вы уже встречались с генераторами. Давайте сравним.
| Характеристика | PHP | Python |
|---|---|---|
| Ключевое слово | yield |
yield |
| Тип генератора | Generator (класс) |
generator (встроенный тип) |
| Создание итератора | Через интерфейс Iterator (5 методов) |
Через методы __iter__ и __next__ (2 метода) |
| Отправка данных в генератор | $gen->send($value) |
gen.send(value) |
| Делегирование | yield from |
yield from |
| Возврат значения | return $value + getReturn() |
return $value + StopIteration.value |
| Генераторные выражения | Нет (только функции) | (x for x in range(10)) |
| Встроенные итераторы | SPL (DirectoryIterator, LimitIterator и др.) | itertools (chain, cycle, groupby и др.) |
Главное отличие: В Python генераторы — это "первоклассные граждане", они используются повсеместно. В PHP генераторы появились позже (в версии 5.5) и до сих пор используются реже, чем могли бы. Многие PHP-разработчики по старинке загружают всё в массивы, хотя генераторы были бы эффективнее.
Ещё отличие: В PHP нет генераторных выражений (аналога (x*2 for x in range(10))). В PHP нужно всегда писать функцию. Но это не страшно — функция даёт больше контроля.
Типичные ошибки и подводные камни
Ошибка 1: Попытка использовать генератор как массив
function myGen(): Generator {
yield 1;
yield 2;
yield 3;
}
$gen = myGen();
echo $gen[0]; // Ошибка! Нельзя обратиться по индексу
echo count($gen); // Ошибка! Нельзя посчитать количество
Решение: Используйте foreach или iterator_to_array() (но осторожно с памятью).
Ошибка 2: Повторное использование генератора
$gen = myGen();
foreach ($gen as $v) { /* первый проход — ок */ }
foreach ($gen as $v) { /* второй проход — пусто! */ }
Генератор нельзя "перемотать" назад. После завершения он пуст. Нужно создавать новый экземпляр.
Решение: Создавайте генератор заново для каждого прохода.
Ошибка 3: Забыли про return в генераторе
function genWithReturn(): Generator {
yield 1;
yield 2;
return 'done';
}
$gen = genWithReturn();
foreach ($gen as $v) {
echo $v; // 1 2
}
// return 'done' не попадёт в foreach!
echo $gen->getReturn(); // 'done' — нужно вызывать отдельно
Ошибка 4: Утечка ресурсов (файлы, соединения)
function badFileReader(string $file): Generator {
$handle = fopen($file, 'r');
while ($line = fgets($handle)) {
yield $line;
}
// Если foreach прервётся досрочно (break), файл не закроется!
fclose($handle);
}
Решение: Используйте try/finally для гарантированного закрытия:
function safeFileReader(string $file): Generator {
$handle = fopen($file, 'r');
try {
while ($line = fgets($handle)) {
yield $line;
}
} finally {
fclose($handle); // Закроется даже при break!
}
}
Ошибка 5: Генератор внутри генератора без yield from
function inner(): Generator {
yield 'внутри';
}
function outer(): Generator {
yield 'снаружи';
yield inner(); // Ошибка! Отдаст объект Generator, а не его значения
}
foreach (outer() as $v) {
echo $v; // 'снаружи', потом объект Generator (не то, что нужно)
}
Решение: Используйте yield from inner().
Лучшие практики и советы профессионалов
-
Используйте генераторы для больших данных. Если вы работаете с файлами, базами данных, API — всегда думайте о генераторах. Они спасут вас от ошибок "out of memory".
-
Не бойтесь создавать свои итераторы. Если логика обхода сложная (деревья, графы, вложенные структуры) — класс-итератор сделает код понятнее и переиспользуемее.
-
Именуйте генераторы с суффиксом Generator. Например,
getUsersGenerator()илиreadLogGenerator(). Это сразу говорит о том, что функция возвращает генератор, а не массив. -
Документируйте тип возврата. Всегда пишите
: Generatorв объявлении функции. Это помогает IDE и другим разработчикам. -
Используйте встроенные итераторы SPL. Прежде чем писать свой итератор, проверьте, нет ли готового в стандартной библиотеке.
DirectoryIterator,RecursiveIteratorIterator,FilterIterator— мощные инструменты. -
Комбинируйте итераторы. Можно обернуть один итератор в другой. Например,
LimitIteratorповерхDirectoryIterator— получите первые 10 файлов. -
Не злоупотребляйте генераторами для маленьких массивов. Если у вас 10 элементов — обычный массив будет быстрее и проще. Генераторы хороши для больших или бесконечных последовательностей.
-
Помните про однократность. Генератор можно обойти только один раз. Если нужно обойти дважды — сохраните результат в массив (если он небольшой) или создайте новый генератор.
Золотое правило: Если вы пишете foreach и внутри него накапливаете данные в массив "на всякий случай" — скорее всего, вам нужен генератор. Он сделает код чище, а память — свободнее.
Вопросы для самопроверки
-
Вопрос: В чём основное отличие итератора от генератора в PHP?
Ответ: Итератор — это класс, реализующий интерфейс Iterator (5 методов). Генератор — это функция с yield, которая автоматически становится объектом класса Generator. Генератор проще писать, но итератор даёт больше