RecursiveIteratorIterator 是一个具体的Iterator 实现tree traversal。它使程序员能够遍历实现RecursiveIterator 接口的容器对象,请参阅Iterator in Wikipedia 了解迭代器的一般原则、类型、语义和模式。
与IteratorIterator 不同,Iterator 以线性顺序实现对象遍历(默认情况下在其构造函数中接受任何类型的Traversable),RecursiveIteratorIterator 允许在 有序树对象及其构造函数采用RecursiveIterator。
简而言之:RecursiveIteratorIterator 允许您遍历树,IteratorIterator 允许您遍历列表。我很快就会用下面的一些代码示例来展示这一点。
从技术上讲,这是通过遍历所有节点的子节点(如果有的话)打破线性来实现的。这是可能的,因为根据定义,一个节点的所有子节点都是RecursiveIterator。顶层Iterator 然后在内部按深度堆叠不同的RecursiveIterators,并保留指向当前活动子Iterator 的指针以供遍历。
这允许访问树的所有节点。
基本原理与IteratorIterator 相同:接口指定迭代的类型,基迭代器类是这些语义的实现。与下面的例子相比,对于foreach 的线性循环,您通常不会过多考虑实现细节,除非您需要定义一个新的Iterator(例如,当某些具体类型本身没有实现Traversable 时)。
对于递归遍历 - 除非您不使用已经具有递归遍历迭代的预定义 Traversal - 您通常需要实例化现有的 RecursiveIteratorIterator 迭代,甚至编写递归遍历Traversable 的迭代是您自己的,以便使用 foreach 进行这种类型的遍历迭代。
提示:你可能没有实现一个或另一个你自己的,所以这可能是你对它们之间差异的实际体验值得做的事情。您会在答案末尾找到一个 DIY 建议。
简而言之技术差异:
- 虽然
IteratorIterator 使用任何Traversable 进行线性遍历,但RecursiveIteratorIterator 需要更具体的RecursiveIterator 来循环遍历树。
-
IteratorIterator 通过getInnerIerator() 公开其主Iterator,RecursiveIteratorIterator 仅通过该方法提供当前活动的子Iterator。
- 虽然
IteratorIterator 完全不知道诸如父母或孩子之类的东西,但RecursiveIteratorIterator 也知道如何获取和遍历孩子。
-
IteratorIterator 不需要迭代器堆栈,RecursiveIteratorIterator 有这样的堆栈并且知道活动的子迭代器。
-
IteratorIterator 由于线性而有其顺序且没有选择,RecursiveIteratorIterator 有进一步遍历的选择,需要根据每个节点进行决定(通过 mode per 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。相反,它只是使用在Traversable 接口上运行的foreach。
由于foreach 默认情况下只知道名为线性顺序的迭代类型,我们可能希望明确指定迭代的类型。乍一看,它可能看起来太冗长,但出于演示目的(并且为了让RecursiveIteratorIterator 的区别在以后更明显),让我们指定迭代的线性类型,明确指定目录列表的IteratorIterator 迭代类型:
$files = new IteratorIterator($dir);
echo "[$path]\n";
foreach ($files as $file) {
echo " ├ $file\n";
}
本示例几乎与第一个示例相同,不同之处在于 $files 现在是 Traversable $dir 的 IteratorIterator 迭代类型:
$files = new IteratorIterator($dir);
像往常一样,迭代的动作是由foreach:
foreach ($files as $file) {
输出一模一样。那么有什么不同呢?不同的是foreach 中使用的对象。在第一个示例中,它是DirectoryIterator,在第二个示例中,它是IteratorIterator。这显示了迭代器的灵活性:您可以将它们相互替换,foreach 中的代码继续按预期工作。
让我们开始获取整个列表,包括子目录。
既然我们现在已经指定了迭代的类型,让我们考虑将其更改为另一种迭代类型。
我们知道我们现在需要遍历整个树,而不仅仅是第一层。要使用简单的foreach 进行这项工作,我们需要一种不同类型的迭代器:RecursiveIteratorIterator。并且只能遍历具有RecursiveIterator interface 的容器对象。
接口是一个合约。任何实现它的类都可以与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
当然这不会赢得选美比赛,但它表明使用递归迭代器可以获得更多信息,而不仅仅是 key 和 value 的线性顺序。即使foreach也只能表达这种线性,访问迭代器本身可以获取更多信息。
与元信息类似,也有不同的方法可以遍历树并因此对输出进行排序。这是Mode of the 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 Work。
示例 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。。