PHP中的RecursiveIteratorIterator如何工作?

95

RecursiveIteratorIterator是如何工作的?

PHP手册中没有很多文档或解释。IteratorIterator和RecursiveIteratorIterator之间有什么区别?


2
这里有一个例子:http://php.net/manual/en/recursiveiteratoriterator.construct.php,还有一个介绍:http://php.net/manual/en/class.iteratoriterator.php - 你能指出你具体不理解的是什么吗?为了更容易理解,手册应该包含哪些内容? - Gordon
1
如果你想知道 RecursiveIteratorIterator 如何工作,那么你是否已经理解了 IteratorIterator 的工作原理呢?我的意思是它们基本上是相同的,只是被两者使用的接口不同而已。你更感兴趣的是一些示例还是想看底层 C 代码实现的差异呢? - hakre
@Gordon 我不确定单个 foreach 循环如何遍历树形结构中的所有元素。 - varuog
@hakra,我现在正在尝试学习所有内置接口以及SPL接口和迭代器实现。我很想知道它们如何在后台与forach循环一起工作,并附带一些示例。 - varuog
@hakre 他们实际上非常不同。IteratorIteratorIteratorIteratorAggregate映射到一个Iterator中,而RecursiveIteratorIterator用于递归遍历RecursiveIterator - Adam
4个回答

277

RecursiveIteratorIterator是一个实现树遍历的具体Iterator。它使程序员能够遍历实现RecursiveIterator接口的容器对象,有关迭代器的一般原则、类型、语义和模式,请参见维基百科中的迭代器

IteratorIterator不同,它是一个按线性顺序实现对象遍历的具体Iterator实现(默认情况下,在其构造函数中接受任何类型的Traversable),RecursiveIteratorIterator允许循环遍历对象的有序树中的所有节点,并且它的构造函数需要一个RecursiveIterator

简而言之:RecursiveIteratorIterator允许您循环遍历树,IteratorIterator允许您循环遍历列表。我将在下面用一些代码示例展示。

从技术上讲,这是通过遍历节点的所有子节点(如果有)来打破线性的方式实现的。这是可能的,因为根据定义,节点的所有子节点再次都是RecursiveIterator。然后,顶层的Iterator通过其深度内部堆叠不同的RecursiveIterator并保持指向当前活动子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决定)。
  • RecursiveIteratorIteratorIteratorIterator拥有更多的方法。
总结一下:RecursiveIterator是迭代的具体类型(遍历树),它可以使用自己的迭代器,即RecursiveIterator。这与IteratorIerator的基本原理相同,但迭代类型不同(线性顺序)。
理想情况下,您也可以创建自己的集合。唯一必要的是,您的迭代器实现了Traversable,这可以通过IteratorIteratorAggregate实现。然后您就可以将其与foreach一起使用。例如,一些三叉树遍历递归迭代对象以及相应的容器对象迭代接口。
让我们通过一些现实生活中的例子来回顾一下,这些例子不那么抽象。在接口、具体迭代器、容器对象和迭代语义之间,这可能不是一个坏主意。
以目录列表为例。考虑你在磁盘上有以下文件和目录树:

Directory Tree

当一个具有线性顺序的迭代器只遍历顶层文件夹和文件(单个目录清单)时,递归迭代器也遍历子文件夹,并列出所有文件夹和文件(带有其子目录列表的目录清单)。
Non-Recursive        Recursive
=============        =========

   [tree]            [tree]
    ├ dirA            ├ dirA
    └ fileA           │ ├ dirB
                      │ │ └ fileD
                      │ ├ fileB
                      │ └ fileC
                      └ fileA

您可以轻松地将其与IteratorIterator进行比较,后者不会递归遍历目录树。而RecursiveIteratorIterator可以像递归列表一样进入树中进行遍历。
首先是一个非常基本的示例,使用实现了TraversableDirectoryIterator,允许foreach来进行迭代:
$path = 'tree';
$dir  = new DirectoryIterator($path);

echo "[$path]\n";
foreach ($dir as $file) {
    echo " ├ $file\n";
}

上述目录结构的示例输出如下:
[tree]
 ├ .
 ├ ..
 ├ dirA
 ├ fileA

你会发现这里还没有使用IteratorIteratorRecursiveIteratorIterator。相反,它只是使用了操作Traversable接口的foreach

由于foreach默认只知道名为线性顺序的迭代类型,我们可能希望明确指定迭代类型。乍一看,它可能过于冗长,但出于演示目的(并为了使与稍后的RecursiveIteratorIterator的差异更加明显),让我们明确指定迭代器类型为IteratorIterator来列出目录清单中的线性迭代类型:

$files = new IteratorIterator($dir);

echo "[$path]\n";
foreach ($files as $file) {
    echo " ├ $file\n";
}

这个例子与第一个例子几乎相同,不同之处在于$files现在是一个IteratorIterator类型的迭代器,用于遍历可遍历的$dir
$files = new IteratorIterator($dir);

通常情况下,迭代的行为由foreach执行:

foreach ($files as $file) {

输出完全相同。那么有什么不同呢?不同的是在foreach内使用的对象。在第一个示例中,它是一个DirectoryIterator,而在第二个示例中,它是IteratorIterator。这显示了迭代器具有的灵活性:您可以将它们彼此替换,foreach内部的代码仍然按预期工作。
让我们开始获取整个列表,包括子目录。
由于我们现在已经指定了迭代类型,让我们考虑将其更改为另一种迭代类型。
我们知道现在需要遍历整个树,而不仅仅是第一级。要使简单的foreach与之配合工作,我们需要一种不同类型的迭代器:RecursiveIteratorIterator。而且,只有实现RecursiveIterator接口的容器对象才能进行迭代。

接口是一种契约。任何实现它的类都可以与RecursiveIteratorIterator一起使用。这样一个类的例子是RecursiveDirectoryIterator,它类似于DirectoryIterator的递归变体。

在写任何其他有关“接口”的句子之前,让我们先看一个代码示例:

$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

这个例子可能不太好看,但是它展示了使用递归迭代器可以获得比仅有keyvalue的线性顺序更多的信息。即使foreach也只能表达这种线性关系,访问迭代器本身可以获取更多的信息。

类似于元信息,遍历树的方式和输出顺序也有不同的可能性。这是RecursiveIteratorIteratorMode模式,可以在构造函数中设置。

下一个例子将告诉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正常工作。

1
它没有回答所提出的问题,包含事实错误,并且在您开始自定义迭代时错过了核心要点。总的来说,它看起来像是一次努力获取赏金的失败尝试,而您对该主题并不了解,或者即使了解也无法将其概括为对所提出问题的答案。 - salathe
2
好吧,这并没有回答我的“为什么”问题,你只是说了更多的话,却没有说太多。也许你可以从实际上计数的错误开始?指出它,不要把它藏起来。 - hakre
而且你忘了提到**RecursiveIteratorIterator如何工作**(例如,它知道如何调用getChildren())。 - salathe
1
@salathe: 感谢您的反馈。我已经对答案进行了编辑以解决这个问题。第一句话确实是错误和不完整的。我仍然省略了RecursiveIteratorIterator的具体实现细节,因为这与其他类型相同,但我确实提供了一些技术信息以说明它的实际工作原理。我认为示例很好地展示了它们之间的差异:迭代类型是它们之间的主要区别。我不知道你是否同意我的说法,但是在语义上,迭代类型有时难以简单地用一种方式表达。 - hakre
1
第一部分有所改进,但一旦开始进入示例,仍然存在事实不准确的问题。如果在水平线处截断答案,它会得到很大的改善。 - salathe
显示剩余5条评论

34

什么是IteratorIteratorRecursiveIteratorIterator之间的区别?

要理解这两个迭代器之间的差异,首先必须了解一些命名约定以及我们所谓的“递归”迭代器。

递归和非递归迭代器

PHP有非“递归”迭代器,例如ArrayIteratorFilesystemIterator。还有诸如RecursiveArrayIteratorRecursiveDirectoryIterator等“递归”迭代器。后者具有使它们能够进入子级的方法,前者则没有。

即使是递归迭代器本身,当对其进行循环遍历时,值也只来自“顶层”,即使对嵌套数组或具有子目录的目录进行循环遍历也是如此。

递归迭代器通过实现hasChildren()getChildren()方法来实现递归行为,但不会利用递归行为。

最好将递归迭代器视为“可递归”迭代器,它们具有被递归迭代的能力,但仅仅迭代这些类的实例并不能做到这一点。要利用递归行为,请继续阅读。

RecursiveIteratorIterator

这就是RecursiveIteratorIterator发挥作用的地方。它知道如何以这样一种方式调用“可递归”迭代器,以便以正常、平坦的循环方式深入到结构中。它把递归行为付诸实践。它基本上是在每个迭代器值上进行步进的工作,查看是否有“子级”需要递归,以及进入和退出那些子级集合。将RecursiveIteratorIterator的实例插入到foreach中,它会自动深入结构,因此您不必自己编写递归循环。

如果不使用RecursiveIteratorIterator,则必须编写自己的递归循环以利用递归行为,检查“可递归”迭代器的hasChildren()并使用getChildren()

因此,这是对RecursiveIteratorIterator简要概述,它与IteratorIterator有何不同?嗯,你基本上是在问与树干区分猫咪和树木的区别。只是因为这两者出现在同一部百科全书(或手册,用于迭代器)中,并不意味着你应该混淆两者。

IteratorIterator

IteratorIterator的任务是获取任何可遍历对象,并包装它以满足Iterator接口。其用途是能够在非迭代对象上应用特定于迭代器的行为。

举个实际例子,DatePeriod类是可遍历的但不是Iterator。因此,我们可以使用foreach()循环遍历其值,但不能像迭代器那样执行其他操作,例如筛选。

任务:$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(…));

正如您所看到的,这与迭代迭代器类或递归完全无关,因此IteratorIteratorRecursiveIteratorIterator之间存在区别。

总结

RecursiveIteraratorIterator 用于遍历“可递归”迭代器RecursiveIterator,利用可用的递归行为。

IteratorIterator 用于将Iterator行为应用于非迭代器Traversable对象。


IteratorIterator不就是Traversable对象的标准线性顺序遍历类型吗?那些可以在foreach中直接使用而不需要它的对象呢?而且,一个RecursiveIterator难道不总是一个Traversable,因此不仅是IteratorIterator,而且也总是“用于将Iterator行为应用于非迭代器、可遍历对象”的RecursiveIteratorIterator吗?(我现在会说,foreach通过容器对象上实现迭代器类型接口的迭代器对象应用迭代类型,因此这些是迭代器容器对象,总是Traversable - hakre
正如我的回答所述,IteratorIterator 是一个类,其主要功能是将 Traversable 对象包装在一个 Iterator 中。没有其他作用。你似乎将该术语应用得更加普遍。 - salathe
8
赞成说"recursible"。因为RecursiveIterator中的“Recursive” 暗示了行为,与之相反,更合适的名称应该描述迭代器的能力,例如RecursibleIterator。因为名字误导了很长时间。请注意我尽力使翻译通俗易懂,但不改变原意。 - goat
在我看来,RecursiveIterators似乎总是需要被RecursiveIteratorIterator包装。如果是这样的话,那么我认为PHP语言应该摆脱公共的RecursiveIteratorIterator类,而是应该返回一个可以直接在foreach循环中递归迭代的迭代器,当你想要使用RecursiveIterators而不使用RecursiveIteratorIterator时,是否存在任何用例呢? - Adam
@Adam:我们无法摆脱公共的RecursiveIteratorIterator类,因为它是递归遍历的事实标准实现,可以与RecursiveIterator接口合同。也许这是一个“你不能既要饼干又想吃掉”的情况。然而,你提出的问题是有依据的。我的建议是:当你已经知道你的类型具有(仅此种类型的)遍历时,将其作为迭代器聚合体(implements IteratorAggregate)并直接从getIterator()方法中提供该迭代器。 - hakre
显示剩余3条评论

0

RecursiveDirectoryIterator会显示整个路径名,而不仅仅是文件名。其他的看起来都很好。这是因为文件名是由SplFileInfo生成的。应该将它们显示为basename。期望的输出如下:

$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

0

当与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));

这是完全误导性的。new IteratorIterator(new ArrayIterator($array)) 等同于 new ArrayIterator($array),也就是说,外部的 IteratorIterator 没有起到任何作用。此外,输出结果的展平与 iterator_to_array 没有任何关系 - 它只是将迭代器转换为数组。展平是由 RecursiveArrayIterator 遍历其内部迭代器的方式决定的属性。 - Quolonel Questions

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接