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

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

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

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

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

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

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

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

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

Для рекурсивного обхода – если вы не используете предопределенный Traversal который уже имеет рекурсивную итерацию обхода, вам обычно необходимо создать экземпляр существующей итерации RecursiveIteratorIterator или даже написать рекурсивную итерацию обхода, которая является Traversable your own, чтобы иметь этот тип итерации обхода с 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 может только выражать такую ​​линейность, доступ к самому итератору позволяет получить дополнительную информацию.

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

В следующем примере команда RecursiveDirectoryIterator будет удалять записи точек ( . И .. ), поскольку они нам не нужны. Но также режим рекурсии будет изменен, чтобы сначала взять родительский элемент (подкаталог) ( SELF_FIRST ) перед 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 (см. Выше): SELF_FIRST каталог, а затем файлы.
  • CHILD_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 $unicodeTreePrefix . ,

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

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

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

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

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

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

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

RecursiveIteratorIterator

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

Если RecursiveIteratorIterator не использовался, вам нужно будет написать свои собственные рекурсивные циклы, чтобы использовать рекурсивное поведение, проверяя на «recursible» hasChildren hasChildren() итератора iterator и используя getChildren() .

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

IteratorIterator

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

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

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

Да, это тривиально для foreach over 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 предназначен для применения поведения итератора к Traversable объектам с возможностью 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));