Как работает 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));