Как работает RecursiveIteratorIterator
?
В руководстве PHP нет ничего документированного или объясненного. В чем разница между IteratorIterator
и RecursiveIteratorIterator
?
Как работает RecursiveIteratorIterator
?
В руководстве PHP нет ничего документированного или объясненного. В чем разница между IteratorIterator
и RecursiveIteratorIterator
?
RecursiveIteratorIterator
является конкретным Iterator
реализуя обход дерева. Это позволяет программисту пересекать объект-контейнер, который реализует интерфейс RecursiveIterator
, см. Iterator в Википедии для общих принципов, типов, семантики и шаблонов итераторов.
В отличие от IteratorIterator
, который представляет собой конкретный обход объекта Iterator
в линейном порядке (и по умолчанию принимает любой тип Traversable
в своем конструкторе), RecursiveIteratorIterator
разрешает цикл по всем узлам в упорядоченном дереве объектов, а его конструктор принимает RecursiveIterator
.
Короче: RecursiveIteratorIterator
позволяет вам перебирать дерево, IteratorIterator
позволяет вам перебирать список. Я покажу, что с некоторыми примерами кода ниже скоро.
Технически это работает путем выхода из линейности путем перемещения всех дочерних узлов (если есть). Это возможно, потому что по определению все дочерние элементы node снова являются a RecursiveIterator
. Верхний слой Iterator
затем внутренне складывает различные RecursiveIterator
по глубине и удерживает указатель на текущий активный суб 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
имеет выбор для дальнейшего обхода и должен решить для каждого node (решено через за 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
, являются частью сущности в приложении: Do It Yourself: выполните RecursiveTreeIterator
Рабочую линию по строке.
В чем разница
IteratorIterator
иRecursiveIteratorIterator
?
Чтобы понять разницу между этими двумя итераторами, нужно сначала немного разобраться в используемых соглашениях об именах и о том, что мы подразумеваем под "рекурсивными" итераторами.
PHP имеет нерекурсивные итераторы, такие как ArrayIterator
и FilesystemIterator
. Существуют также "рекурсивные" итераторы, такие как RecursiveArrayIterator
и RecursiveDirectoryIterator
. У последних есть методы, позволяющие их сверлить, первые не делают.
Когда экземпляры этих итераторов зацикливаются сами по себе, даже рекурсивные, значения берутся только с "верхнего" уровня, даже если они зацикливаются на вложенном массиве или каталоге с подкаталогами.
Рекурсивные итераторы реализуют рекурсивное поведение (через hasChildren()
, getChildren()
), но не используют его.
Лучше подумать о рекурсивных итераторах как "рекурсивных" итераторах, они могут быть рекурсивно повторены, но просто повторение экземпляра одного из этих классов этого не сделает. Чтобы использовать рекурсивное поведение, продолжайте читать.
Здесь находится RecursiveIteratorIterator
. Он знает, как назвать "рекурсивные" итераторы таким образом, чтобы развернуть структуру в нормальном, плоском цикле. Это ставит рекурсивное поведение в действие. Это, по сути, делает работу по переходу на каждое из значений в итераторе, глядя, есть ли "дети" для рекурсии или нет, и вступая в и выходя из этих коллекций детей. Вы вставляете экземпляр RecursiveIteratorIterator
в foreach, и он погружается в структуру, так что вам не нужно.
Если RecursiveIteratorIterator
не использовался, вам придется написать свои собственные рекурсивные циклы, чтобы использовать рекурсивное поведение, проверяя "рекурсивный" итератор hasChildren()
и используя getChildren()
.
Итак, краткий обзор RecursiveIteratorIterator
, как он отличается от 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));