Как работает RecursiveIteratorIterator в PHP?

Как RecursiveIteratorIterator Работа?

В руководстве по PHP ничего особо не задокументировано и не объяснено. В чем разница между IteratorIterator а также RecursiveIteratorIterator?

4 ответа

Решение

RecursiveIteratorIterator это бетон Iterator реализация обхода дерева. Это позволяет программисту проходить контейнерный объект, который реализует RecursiveIterator Интерфейс, см. Итератор в Википедии для общих принципов, типов, семантики и шаблонов итераторов.

В отличии от IteratorIterator который является конкретным Iterator реализация обхода объекта в линейном порядке (и по умолчанию принимает любой вид Traversable в своем конструкторе), RecursiveIteratorIterator позволяет зацикливаться на всех узлах в упорядоченном дереве объектов, и его конструктор принимает RecursiveIterator,

Короче: RecursiveIteratorIterator позволяет зацикливаться на дереве, IteratorIterator позволяет перебирать список. Я покажу это с некоторыми примерами кода ниже.

Технически это работает путем нарушения линейности путем обхода всех дочерних узлов (если они есть). Это возможно, потому что по определению все дочерние узлы снова являются RecursiveIterator, Верхний уровень Iterator затем внутренне складывает разные RecursiveIterator s по глубине и держит указатель на текущий активный саб Iterator для прохождения.

Это позволяет посещать все узлы дерева.

Основополагающие принципы такие же, как с IteratorIterator Интерфейс определяет тип итерации, а базовый класс итератора является реализацией этой семантики. Сравните с примерами ниже, для линейного цикла с foreach вы обычно не задумываетесь о деталях реализации, если вам не нужно определять новый Iterator (например, когда какой-то конкретный тип сам по себе не реализует Traversable).

Для рекурсивного обхода - если вы не используете предварительно определенный Traversal который уже имеет рекурсивную итерацию обхода - вам обычно нужно создать экземпляр существующего RecursiveIteratorIterator итерации или даже написать рекурсивную итерацию обхода, которая является Traversable свой собственный, чтобы иметь этот тип обхода итерации с foreach,

Совет: вы, вероятно, не реализовали ни то, ни другое самостоятельно, так что это может быть чем-то полезным для вашего практического опыта различий, которые они имеют. Вы найдете предложение DIY в конце ответа.

Короче технические отличия:

  • В то время как IteratorIterator принимает любой Traversable для линейного обхода, RecursiveIteratorIterator нужен более конкретный RecursiveIterator зацикливаться на дереве.
  • куда IteratorIterator выставляет свою главную Iterator с помощью getInnerIerator(), RecursiveIteratorIterator обеспечивает текущий активный суб- Iterator только через этот метод.
  • В то время как IteratorIterator совершенно ничего не знает о родителях или детях, RecursiveIteratorIterator знает, как получить и пройти детей.
  • IteratorIterator не нужен стек итераторов, RecursiveIteratorIterator имеет такой стек и знает активный итератор.
  • куда IteratorIterator имеет свой порядок из-за линейности и нет выбора, RecursiveIteratorIterator имеет выбор для дальнейшего прохождения и должен решить для каждого узла (определяется через режим для RecursiveIteratorIterator).
  • RecursiveIteratorIterator имеет больше методов, чем IteratorIterator,

Подвести итоги: RecursiveIterator это конкретный тип итерации (цикл по дереву), который работает на своих собственных итераторах, а именно RecursiveIterator, Это тот же основной принцип, что и с IteratorIerator, но тип итерации другой (линейный порядок).

В идеале вы также можете создать свой собственный набор. Единственное, что нужно, это то, что ваш итератор реализует Traversable что возможно через Iterator или же IteratorAggregate, Тогда вы можете использовать его с foreach, Например, некоторый вид рекурсивного итерационного объекта обхода троичного дерева вместе с соответствующим интерфейсом итерации для объекта (ов) контейнера.


Давайте рассмотрим примеры из жизни, которые не настолько абстрактны. Между интерфейсами, конкретными итераторами, объектами-контейнерами и семантикой итераций это может быть не такой уж плохой идеей.

Возьмите список каталогов в качестве примера. Предположим, у вас есть следующий файл и дерево каталогов на диске:

Дерево каталогов

В то время как итератор с линейным порядком просто пересекает папку и файлы верхнего уровня (один список каталогов), рекурсивный итератор также проходит по подпапкам и выводит список всех папок и файлов (список каталогов со списками своих подкаталогов):

Non-Recursive        Recursive
=============        =========

   [tree]            [tree]
    ├ dirA            ├ dirA
    └ fileA           │ ├ dirB
                      │ │ └ fileD
                      │ ├ fileB
                      │ └ fileC
                      └ fileA

Вы можете легко сравнить это с IteratorIterator который не делает рекурсии для обхода дерева каталогов. И RecursiveIteratorIterator который может пройти в дерево, как показывает рекурсивный листинг.

Сначала очень простой пример с DirectoryIterator который реализует Traversable который позволяет foreach перебрать его:

$path = 'tree';
$dir  = new DirectoryIterator($path);

echo "[$path]\n";
foreach ($dir as $file) {
    echo " ├ $file\n";
}

Примерный вывод для структуры каталогов выше:

[tree]
 ├ .
 ├ ..
 ├ dirA
 ├ fileA

Как вы видите, это еще не использует IteratorIterator или же RecursiveIteratorIterator, Вместо этого он просто использует foreach который работает на Traversable интерфейс.

Как foreach по умолчанию известен только тип итерации с именем линейный порядок, мы можем явно указать тип итерации. На первый взгляд это может показаться слишком многословным, но для демонстрационных целей (и для RecursiveIteratorIterator более заметным позже), давайте определим линейный тип итерации, явно указав IteratorIterator тип итерации для списка каталогов:

$files = new IteratorIterator($dir);

echo "[$path]\n";
foreach ($files as $file) {
    echo " ├ $file\n";
}

Этот пример практически идентичен первому, разница в том, что $files сейчас IteratorIterator тип итерации для Traversable$dir:

$files = new IteratorIterator($dir);

Как обычно, акт итерации выполняется foreach:

foreach ($files as $file) {

Выход точно такой же. Так что же отличается? Отличным является объект, используемый в foreach, В первом примере это DirectoryIterator во втором примере это IteratorIterator, Это показывает гибкость, которую имеют итераторы: вы можете заменить их друг на друга, код внутри foreach просто продолжай работать как положено.

Давайте начнем получать весь список, включая подкаталоги.

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

Мы знаем, что теперь нам нужно пройти через все дерево, а не только на первый уровень. Чтобы это работало с простым foreach нам нужен другой тип итератора: RecursiveIteratorIterator, И что можно перебирать только объекты-контейнеры, которые имеют RecursiveIterator интерфейс

Интерфейс является контрактом. Любой класс, реализующий его, может использоваться вместе с RecursiveIteratorIterator, Примером такого класса является RecursiveDirectoryIterator что-то вроде рекурсивного варианта DirectoryIterator,

Давайте посмотрим первый пример кода, прежде чем писать любое другое предложение с I-словом:

$dir  = new RecursiveDirectoryIterator($path);

echo "[$path]\n";
foreach ($dir as $file) {
    echo " ├ $file\n";
}

Этот третий пример почти идентичен первому, однако он создает несколько иной вывод:

[tree]
 ├ tree\.
 ├ tree\..
 ├ tree\dirA
 ├ tree\fileA

Ладно, не так уж и отличается, имя файла теперь содержит путь впереди, но все остальное выглядит так же.

Как показывает пример, даже объект каталога уже реализует RecursiveIterator интерфейс, этого еще недостаточно, чтобы сделать foreach пройти через все дерево каталогов. Это где RecursiveIteratorIterator вступает в действие. Пример 4 показывает, как:

$files = new RecursiveIteratorIterator($dir);

echo "[$path]\n";
foreach ($files as $file) {
    echo " ├ $file\n";
}

С использованием RecursiveIteratorIterator вместо просто предыдущего $dir объект сделает foreach рекурсивно обходить все файлы и каталоги. Затем перечисляются все файлы, так как тип итерации объекта был указан сейчас:

[tree]
 ├ tree\.
 ├ tree\..
 ├ tree\dirA\.
 ├ tree\dirA\..
 ├ tree\dirA\dirB\.
 ├ tree\dirA\dirB\..
 ├ tree\dirA\dirB\fileD
 ├ tree\dirA\fileB
 ├ tree\dirA\fileC
 ├ tree\fileA

Это уже должно продемонстрировать разницу между плоским и обходом дерева. RecursiveIteratorIterator способен обходить любую древовидную структуру в виде списка элементов. Поскольку имеется больше информации (например, уровень, который занимает итерация в настоящее время), можно получить доступ к объекту итератора при итерации по нему и, например, сделать отступ для выходных данных:

echo "[$path]\n";
foreach ($files as $file) {
    $indent = str_repeat('   ', $files->getDepth());
    echo $indent, " ├ $file\n";
}

И вывод примера 5:

[tree]
 ├ tree\.
 ├ tree\..
    ├ tree\dirA\.
    ├ tree\dirA\..
       ├ tree\dirA\dirB\.
       ├ tree\dirA\dirB\..
       ├ tree\dirA\dirB\fileD
    ├ tree\dirA\fileB
    ├ tree\dirA\fileC
 ├ tree\fileA

Конечно, это не выигрывает конкурс красоты, но показывает, что с помощью рекурсивного итератора доступно больше информации, чем просто линейный порядок ключа и значения. Четное foreach Можно выразить только такую ​​линейность, доступ к самому итератору позволяет получить больше информации.

Подобно метаинформации, существуют также различные способы прохождения дерева и, следовательно, упорядочения вывода. Это режим RecursiveIteratorIterator и это может быть установлено с помощью конструктора.

Следующий пример расскажет RecursiveDirectoryIterator удалить точечные записи (. а также ..) так как они нам не нужны. Но также режим рекурсии будет изменен, чтобы сначала взять родительский элемент (подкаталог) (SELF_FIRST) перед потомками (файлы и подкаталоги в подкаталоге):

$dir  = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::SELF_FIRST);

echo "[$path]\n";
foreach ($files as $file) {
    $indent = str_repeat('   ', $files->getDepth());
    echo $indent, " ├ $file\n";
}

Вывод теперь показывает записи в подкаталоге должным образом в списке, если сравнить с предыдущими выводами, которых там не было:

[tree]
 ├ tree\dirA
    ├ tree\dirA\dirB
       ├ tree\dirA\dirB\fileD
    ├ tree\dirA\fileB
    ├ tree\dirA\fileC
 ├ tree\fileA

Таким образом, режим рекурсии управляет тем, что и когда возвращается скобка или лист в дереве, для примера каталога:

  • LEAVES_ONLY (по умолчанию): только список файлов, без каталогов.
  • SELF_FIRST (выше): список каталогов, а затем файлы там.
  • CHILD_FIRST (без примера): сначала перечислите файлы в подкаталоге, затем в каталоге.

Вывод примера 5 с двумя другими режимами:

  LEAVES_ONLY                           CHILD_FIRST

  [tree]                                [tree]
         ├ tree\dirA\dirB\fileD                ├ tree\dirA\dirB\fileD
      ├ tree\dirA\fileB                     ├ tree\dirA\dirB
      ├ tree\dirA\fileC                     ├ tree\dirA\fileB
   ├ tree\fileA                             ├ tree\dirA\fileC
                                        ├ tree\dirA
                                        ├ tree\fileA

Если сравнить это со стандартным обходом, все эти вещи недоступны. Поэтому рекурсивная итерация немного сложнее, когда вам нужно обернуть ее вокруг, однако ее легко использовать, поскольку она ведет себя так же, как итератор, вы помещаете ее в foreach и сделано.

Я думаю, что это достаточно примеров для одного ответа. Вы можете найти полный исходный код, а также пример для отображения красивых деревьев ascii в этом списке: https://gist.github.com/3599532

Сделай сам RecursiveTreeIterator Работа Линия за Линейкой.

Пример 5 продемонстрировал, что имеется метаинформация о состоянии итератора. Однако это было целенаправленно продемонстрировано в рамках foreach итерация. В реальной жизни это естественно принадлежит внутри RecursiveIterator,

Лучшим примером является RecursiveTreeIterator, он заботится о отступах, префиксах и так далее. Смотрите следующий фрагмент кода:

$dir   = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$lines = new RecursiveTreeIterator($dir);
$unicodeTreePrefix($lines);
echo "[$path]\n", implode("\n", iterator_to_array($lines));

RecursiveTreeIterator предназначен для работы построчно, вывод довольно прост с одной маленькой проблемой:

[tree]
 ├ tree\dirA
 │ ├ tree\dirA\dirB
 │ │ └ tree\dirA\dirB\fileD
 │ ├ tree\dirA\fileB
 │ └ tree\dirA\fileC
 └ tree\fileA

При использовании в сочетании с RecursiveDirectoryIterator он отображает полное имя пути, а не только имя файла. В остальном выглядит хорошо. Это потому, что имена файлов генерируются SplFileInfo, Они должны отображаться как базовое имя. Желаемый результат следующий:

/// Solved ///

[tree]
 ├ dirA
 │ ├ dirB
 │ │ └ fileD
 │ ├ fileB
 │ └ fileC
 └ fileA

Создать класс декоратора, который можно использовать с RecursiveTreeIterator вместо RecursiveDirectoryIterator, Он должен предоставить базовое имя текущего SplFileInfo вместо пути Окончательный фрагмент кода может выглядеть следующим образом:

$lines = new RecursiveTreeIterator(
    new DiyRecursiveDecorator($dir)
);
$unicodeTreePrefix($lines);
echo "[$path]\n", implode("\n", iterator_to_array($lines));

Эти фрагменты в том числе $unicodeTreePrefix являются частью сути в приложении: Сделай сам: сделай RecursiveTreeIterator Работа Линия за Линейкой.,

В чем разница IteratorIterator а также RecursiveIteratorIterator?

Чтобы понять разницу между этими двумя итераторами, нужно сначала немного понять используемые соглашения об именах и то, что мы подразумеваем под "рекурсивными" итераторами.

Рекурсивные и нерекурсивные итераторы

PHP имеет нерекурсивные итераторы, такие как ArrayIterator а также FilesystemIterator, Есть также "рекурсивные" итераторы, такие как RecursiveArrayIterator а также RecursiveDirectoryIterator, У последних есть методы, позволяющие их детализировать, у первых нет.

Когда экземпляры этих итераторов зацикливаются сами по себе, даже рекурсивные, значения поступают только с "верхнего" уровня, даже если они зацикливаются на вложенном массиве или каталоге с подкаталогами.

Рекурсивные итераторы реализуют рекурсивное поведение (через hasChildren(), getChildren()) но не эксплуатируйте его.

Возможно, было бы лучше думать о рекурсивных итераторах как о "рекурсивных" итераторах, они могут рекурсивно повторяться, но простая итерация по экземпляру одного из этих классов не сделает этого. Чтобы использовать рекурсивное поведение, продолжайте читать.

RecursiveIteratorIterator

Это где RecursiveIteratorIterator приходит играть Он знает, как вызывать "рекурсивные" итераторы таким образом, чтобы развернуть структуру в нормальном плоском цикле. Это приводит рекурсивное поведение в действие. По сути, он выполняет работу по переходу через каждое из значений в итераторе, проверяя, есть ли "дочерние элементы", в которые можно вернуться, и входя и выходя из этих коллекций дочерних элементов. Вы прикрепляете экземпляр RecursiveIteratorIterator в foreach, и он погружается в структуру, так что вам не нужно.

Если RecursiveIteratorIterator не использовался, вы должны были бы написать свои собственные рекурсивные циклы, чтобы использовать рекурсивное поведение, проверяя "рекурсивный" итератор hasChildren() и используя getChildren(),

Итак, это краткий обзор RecursiveIteratorIterator чем он отличается от IteratorIterator? Ну, вы в основном задаете тот же вопрос, что и Какая разница между котенком и деревом? То, что оба они появляются в одной и той же энциклопедии (или в руководстве для итераторов), не означает, что вы должны запутаться между ними.

IteratorIterator

Работа IteratorIterator это взять любой Traversable объект, и оберните его так, чтобы он удовлетворял Iterator интерфейс. В этом случае можно применить поведение, специфичное для итератора, к неитераторному объекту.

Чтобы привести практический пример, DatePeriod класс Traversable но не Iterator, Таким образом, мы можем зациклить его значения с foreach() но не может делать другие вещи, которые мы обычно делаем с итератором, такие как фильтрация.

ЗАДАЧА: Цикл по понедельникам, средам и пятницам следующих четырех недель.

Да, это тривиально foreach над DatePeriod и используя if() в цикле; но дело не в этом примере!

$period = new DatePeriod(new DateTime, new DateInterval('P1D'), 28);
$dates  = new CallbackFilterIterator($period, function ($date) {
    return in_array($date->format('l'), array('Monday', 'Wednesday', 'Friday'));
});
foreach ($dates as $date) { … }

Вышеуказанный фрагмент не будет работать, потому что CallbackFilterIterator ожидает экземпляр класса, который реализует Iterator интерфейс, который DatePeriod не. Тем не менее, так как это Traversable мы можем легко удовлетворить это требование, используя IteratorIterator,

$period = new IteratorIterator(new DatePeriod(…));

Как вы можете видеть, это не имеет ничего общего ни с итерацией по классам итераторов, ни с рекурсией, и в этом заключается разница между IteratorIterator а также RecursiveIteratorIterator,

Резюме

RecursiveIteraratorIterator для итерации по RecursiveIterator ("рекурсивный" итератор), использующий рекурсивное поведение, которое доступно.

IteratorIterator для применения Iterator Поведение без итератора, Traversable объекты.

RecursiveDirectoryIterator отображает полное имя пути, а не только имя файла. В остальном выглядит хорошо. Это потому, что имена файлов генерируются SplFileInfo. Они должны отображаться как базовое имя. Желаемый результат следующий:

$path =__DIR__;
$dir = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($dir,RecursiveIteratorIterator::SELF_FIRST);
while ($files->valid()) {
    $file = $files->current();
    $filename = $file->getFilename();
    $deep = $files->getDepth();
    $indent = str_repeat('│ ', $deep);
    $files->next();
    $valid = $files->valid();
    if ($valid and ($files->getDepth() - 1 == $deep or $files->getDepth() == $deep)) {
        echo $indent, "├ $filename\n";
    } else {
        echo $indent, "└ $filename\n";
    }
}

выход:

tree
 ├ dirA
 │ ├ dirB
 │ │ └ fileD
 │ ├ fileB
 │ └ fileC
 └ fileA

При использовании с iterator_to_array(), RecursiveIteratorIterator будет рекурсивно обходить массив, чтобы найти все значения. Это означает, что он сгладит исходный массив.

IteratorIterator сохранит исходную иерархическую структуру.

Этот пример покажет вам разницу:

$array = array(
               'ford',
               'model' => 'F150',
               'color' => 'blue', 
               'options' => array('radio' => 'satellite')
               );

$recursiveIterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
var_dump(iterator_to_array($recursiveIterator, true));

$iterator = new IteratorIterator(new ArrayIterator($array));
var_dump(iterator_to_array($iterator,true));
Другие вопросы по тегам