Как работает 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
в foreach, и он погружается в структуру, так что вам не нужно.
Если RecursiveIteratorIterator
не использовался, вам нужно будет написать свои собственные рекурсивные циклы, чтобы использовать рекурсивное поведение, проверяя на «recursible» hasChildren hasChildren()
итератора iterator и используя getChildren()
.
Итак, это краткий обзор RecursiveIteratorIterator
, как он отличается от 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));