Рубрики
Uncategorized

Итераторы и генераторы в PHP

Советы и рекомендации по использованию итератора и генератора в реальном мире. Помеченный php, итераторами, генераторами.

В этом посте я демонстрирую эффективный способ создания итераторов и генераторов в 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”