В этом посте я демонстрирую эффективный способ создания итераторов и генераторов в PHP и привожу пример сценария, в котором их использование имеет смысл.
Генераторы существуют с PHP 5.5, и итераторы существуют со времен эпохи Планка. Тем не менее, многие разработчики PHP не знают, как их правильно использовать, и не могут распознать ситуации, в которых они полезны. В этом посте в блоге я делюсь идеями, которые я приобрел за эти годы, и, делясь ими, всегда получал заинтересованный отклик от коллег-разработчиков. Этот пост выходит за рамки основ, содержит пример из реального мира и включает в себя несколько советов и рекомендаций. Чтобы не пропустить тех, кто не знаком с итераторами, сообщение начинается с раздела “Что такое итераторы”, который вы можете смело пропустить, если уже можете ответить на этот вопрос.
Что такое итераторы
PHP имеет интерфейс Итератор , который вы можете реализовать для представления коллекции. Вы можете выполнить цикл над экземпляром итератора точно так же, как вы можете выполнить цикл над массивом:
function doStuff(Iterator $things) {
foreach ($things as $thing) { /* ... */ }
}
Зачем вам беспокоиться о реализации подкласса Итератор , а не просто использовать массив? Давайте рассмотрим пример.
Представьте, что у вас есть каталог с кучей текстовых файлов. Один из файлов содержит ASCII-няньскую кошку ( ~=[,,_,,]:3 ). Задача нашего кода – найти, в каком файле прячется кот Ньян.
Мы можем получить все файлы, выполнив glob($путь.' *.текст') и мы можем получить содержимое файла с помощью file_get_contents . Мы могли бы просто получить результат для каждого по всему миру, который выполняет file_get_contents . К счастью, мы понимаем, что это нарушит разделение проблем и затруднит проверку логики “содержит ли этот файл Nyan Cat”, поскольку она будет привязана к коду доступа к файловой системе. Следовательно, мы создаем функцию, которая получает содержимое файлов и файлов с нашей логикой в них:
function getContentsOfTextFiles(): array {
// glob and file_get_contents
}
function findTextWithNyanCat(array $texts) {
foreach ($texts as $text) { if ( /* ... */ ) { /* ... */ } }
}
function findNyanCat() {
findTextWithNyanCat(getContentsOfTextFiles());
}
Хотя этот подход развязан , большим недостатком является то, что теперь нам нужно извлечь содержимое всех файлов и сохранить все это в памяти , прежде чем мы даже начнем выполнять какую-либо из наших логик. Если Ньян Кэт прячется в первом файле, мы зря извлекли содержимое всех остальных. Мы можем избежать этого, используя Итератор , так как они могут извлекать свои значения по требованию: они ленивы .
class TextFileIterator implements Iterator {
/* ... */
public function current() {
// return file_get_contents
}
/* ... */
}
function findTextWithNyanCat(Iterator $texts) {
foreach ($texts as $text) { if ( /* ... */ ) { /* ... */ } }
}
function findNyanCat() {
findTextWithNyanCat(new TextFileIterator());
}
Наш TextFileIterator дает нам хорошее место для размещения всего кода файловой системы, в то время как снаружи он выглядит просто как набор текстов. Функция, в которой содержится наша логика, найти текст с помощью Nyan Cat , не знает, что текст поступает из файловой системы. Это означает, что если вы решите получать тексты из базы данных, вы можете просто создать новый итератор больших двоичных объектов текста базы данных и передать его в логическую функцию без внесения каких-либо изменений в последнюю. Аналогично, при тестировании логической функции вы можете присвоить ей ArrayIterator .
function testFindTextWithNyanCat() {
/* ... */
findTextWithNyanCat(new ArrayIterator(['test text', '~=[,,_,,]:3']));
/* ... */
}
Я написал больше об основном Итератор функциональность в Ленивые итераторы в PHP и Python и Немного повеселиться с итераторами . Я также написал в блоге о библиотеке , которая предоставляет некоторые (специфичные для викиданных) итераторы и инструмент CLI, построенный вокруг итератора . Для получения дополнительной информации о том, как работают генераторы, см. Сообщение за пределами сайта Генераторы в PHP .
Иерархия типов коллекций PHP
Давайте начнем с рассмотрения иерархии типов PHP для коллекций, начиная с PHP 7.1. Это основные типы, которые я считаю наиболее важными:
На самом верху у нас есть итерируемый , супертип обоих массивов и Проходимый . Если вы не знакомы с этим типом или используете версию PHP старше 7.1, не волнуйтесь, нам это не нужно для остальной части этого сообщения в блоге.
Итератор является подтипом Проходимый , и то же самое относится к IteratorAggregate . Стандартная библиотека iterator_ функции, такие как iterator_to_array все берут Проходимый . Это важно, так как это означает, что вы можете дать им IteratorAggregate , даже если это не Итератор . Позже в этом посте мы вернемся к тому, что такое IteratorAggregate и почему он полезен.
Наконец-то у нас есть Генератор , который является подтипом Итератор . Это означает, что всем функциям, которые принимают Итератор , может быть присвоен Генератор , и, соответственно, вы можете использовать генераторы в сочетании с классами Итераторов в стандартной библиотеке PHP , такими как Ограничитель и Кэширующий генератор .
Итератораагрегат + Генератор = <3
Генераторы – это хороший и простой способ создания итераторов. Часто вы просматриваете их только один раз, и у вас не возникает никаких проблем. Однако имейте в виду, что генераторы создают итераторы, которые нельзя перематывать, что означает, что если вы повторите их несколько раз, вы получите исключение.
Представьте себе сценарий, в котором вы передаете генератор в службу, которая принимает экземпляр Проходимый :
$aGenerator = function() { /* ... yield ... */ };
$aService->doStuff($aGenerator());
public function doStuff(Traversable $things) {
foreach ($things as $thing) { /* ... */ }
}
Класс обслуживания, в котором делать что-то он не знает, что получает Генератор , он просто знает, что получает Проходимый . При работе над этим классом вполне разумно повторить $things во второй раз.
public function doStuff(Traversable $things) {
foreach ($things as $thing) { /* ... */ }
foreach ($things as $thing) { /* ... */ } // Boom if Generator!
}
Это взорвется, если предоставленные $вещи является Генератором , потому что генераторы не подлежат возврату. Обратите внимание, что не имеет значения, как вы повторяете значение. Вызов iterator_to_array с помощью $things дает тот же результат, что и при использовании в цикле foreach. Большинство, если не все, генераторы, которые я написал, не используют ресурсы или состояния, которые по своей сути препятствуют их перемотке. Таким образом, проблема с двойной итерацией может быть неожиданной и, казалось бы, глупой.
Однако есть простой и легкий способ обойти это. Вот где Входит IteratorAggregate . Классы, реализующие IteratorAggregate должен реализовать метод getIterator() , который возвращает Проходимый . Создание одного из них чрезвычайно тривиально:
class AwesomeWords implements \IteratorAggregate {
public function getIterator() {
yield 'So';
yield 'Much';
yield 'Such';
}
}
Если вы вызовете getIterator , вы получите Генератор экземпляр, как и следовало ожидать. Однако обычно вы никогда не вызываете этот метод. Вместо этого вы используете IteratorAggregate так же, как если бы это был Итератор , передавая его в код, который ожидает Проходимый . (Именно поэтому обычно вы хотите принять Проходимый и не только Итератор .) Теперь мы можем позвонить в наш сервис, который дважды повторяет $вещи без каких-либо проблем:
$aService->doStuff(new AwesomeWords()); // no boom!
С помощью Итератораагрегировать мы не просто решили проблему с невозможностью перемотки, мы также нашли хороший способ поделиться нашим кодом. Иногда имеет смысл использовать код Генератора в нескольких классах, а иногда имеет смысл иметь специальные тесты для |/Генератора . В обоих случаях наличие выделенного класса и файла для его размещения очень полезно и намного приятнее, чем предоставление генератора с помощью какой-либо общедоступной статической функции.
Для случаев, когда нет смысла делиться генератором и если вы хотите сохранить это в секрете, вам может потребоваться решить проблему, не подлежащую перемотке. Для этих случаев вы можете использовать мою библиотеку Перематываемый генератор , которая позволяет сделать ваши генераторы перематываемыми, обернув их функцию создания:
$aGenerator = function() { /* ... yield ... */ };
$aService->doStuff(new RewindableGenerator($aGenerator));
Пример из реального мира
Несколько месяцев назад я переработал некоторую часть кода Базы данных по сбору средств Викимедиа в Германии . Этот код получает пути к файловой системе шаблонов электронной почты, просматривая набор указанных каталогов.
private function getMailTemplatesOnDisk( array $mailTemplatePaths ): array {
$mailTemplatesOnDisk = [];
foreach ( $mailTemplatePaths as $path ) {
$mailFilesInFolder = glob( $path . '/Mail_*' );
array_walk( $mailFilesInFolder, function( & $filename ) {
$filename = basename( $filename ); // this would cause problems w/ mail templates in sub-folders
} );
$mailTemplatesOnDisk = array_merge( $mailTemplatesOnDisk, $mailFilesInFolder );
}
return $mailTemplatesOnDisk;
}
Этот код сделал класс привязанным к файловой системе, что затруднило его тестирование. На самом деле этот код не был протестирован. Кроме того, этот код раздражал меня, так как мне нравится, чтобы код был функциональным. В array_walk изменяет свою переменную по ссылке, а назначение в конце цикла изменяет возвращаемую переменную.
Это было переработано с помощью потрясающего Итератораагрегировать + Генератор комбинированный:
class MailTemplateFilenameTraversable implements \IteratorAggregate {
public function __construct( array $mailTemplatePaths ) {
$this->mailTemplatePaths = $mailTemplatePaths;
}
public function getIterator() {
foreach ( $this->mailTemplatePaths as $path ) {
foreach ( glob( $path . '/Mail_*' ) as $fileName ) {
yield basename( $fileName );
}
}
}
}
Гораздо проще читать/понимать код, никаких изменений состояния, хорошее разделение проблем, более простое тестирование и возможность повторного использования этого кода построения коллекции в других местах.
См. также: Примеры использования для PHP-генераторов (сообщение за пределами сайта).
Советы и рекомендации
Генераторы могут выдавать пары ключевых значений:
yield "Iterators" => "are useful"; yield "Generators" => "are awesome"; // [ "Iterators" => "are useful", "Generators" => "are awesome" ]
Вы можете использовать выход в поставщиках данных PHPUnit . Вы можете выйти из /итерируемого .
yield from [1, 2, 3]; yield from new ArrayIterator([4, 5]); // 1, 2, 3, 4, 5
// Flattens iterable[] into Generator
foreach ($collections as $collection) {
yield from $collection;
}
Спасибо Лешеку Маницки и Яну Диттриху за рецензирование этого поста в блоге.
Первоначально размещенный на моем блог как Введение в итераторы и генераторы в PHP .
Оригинал: “https://dev.to/jeroendedauw/iterators-and-generators-in-php-e3p”