RecursiveIteratorIterator如何在PHP中工作?


88

RecursiveIteratorIterator工作如何?

PHP手册没有太多文档记录或解释。IteratorIterator和之间有什么区别RecursiveIteratorIterator


2
有一个例子php.net/manual/en/recursiveiteratoriterator.construct.php并且也有一个介绍,在php.net/manual/en/class.iteratoriterator.php -可以请你指出究竟你很难理解。手册应包含哪些内容以便更容易掌握?
Gordon

1
如果您问RecursiveIteratorIterator工作原理,您是否已经了解IteratorIterator工作原理?我的意思是基本相同,只是两者所使用的接口不同。您是否对某些示例更感兴趣,还是想看一下底层C代码实现的区别?
hakre 2012年

@戈登,我不确定单个foreach循环如何遍历树结构中的所有元素
varuog 2012年

@hakra我现在正在尝试研究所有内置接口以及spl接口和迭代器的实现。
varuog 2012年

@hakre实际上两者都非常不同。IteratorIterator地图IteratorIteratorAggregateIterator,在那里REcusiveIteratorIterator被用来recusivly遍历RecursiveIterator
亚当

Answers:


249

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

与以线性顺序遍历IteratorIterator具体Iterator实现的对象遍历(默认情况下,Traversable在其构造函数中接受任何形式的RecursiveIteratorIterator遍历)不同,允许循环遍历对象的有序树中的所有节点,并且构造函数采用RecursiveIterator

简而言之:RecursiveIteratorIterator允许您遍历树,IteratorIterator允许您遍历列表。我很快在下面的一些代码示例中对此进行了展示。

从技术上讲,这可以通过遍历所有节点的子节点(如果有)来打破线性关系而起作用。这是可能的,因为根据定义,节点的所有子节点都还是a RecursiveIterator。然后,顶层按深度Iterator在内部堆叠不同RecursiveIterator的,并保持指向当前活动子的指针以Iterator进行遍历。

这允许访问树的所有节点。

基本原理与相同IteratorIterator:接口指定迭代的类型,基本迭代器类是这些语义的实现。与下面的示例进行比较,对于与foreach您的线性循环,除非您需要定义一个新的Iterator(例如,当某些具体类型本身未实现时Traversable),否则通常不会过多地考虑实现细节。

对于递归遍历-除非您不使用Traversal已经具有递归遍历迭代的预定义-通常,您需要实例化现有RecursiveIteratorIterator迭代,甚至编写自己的递归遍历迭代,Traversable以使用此类遍历迭代foreach

提示:您可能没有实现自己的实现,也可能没有实现,因此对于您实际了解它们之间的差异可能是值得做的事情。您会在答案的结尾找到一个自己动手的建议。

简而言之,技术差异:

  • 虽然IteratorIterator需要Traversable进行线性遍历,但是RecursiveIteratorIterator需要更具体RecursiveIterator地遍历树。
  • IteratorIterator公开其主要Iteratorvia的地方getInnerIerator(),仅通过该方法RecursiveIteratorIterator提供当前活动的子Iterator
  • 虽然IteratorIterator完全不了解父母或孩子,但RecursiveIteratorIterator也知道如何获得和穿越孩子。
  • IteratorIterator不需要堆栈的迭代器,RecursiveIteratorIterator具有这样的堆栈,并且知道活动的子迭代器。
  • 其中IteratorIterator由于线性而具有顺序,并且没有选择权,RecursiveIteratorIterator可以选择进一步遍历,并且需要根据每个节点来决定(通过模式perRecursiveIteratorIterator来决定)。
  • 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

如您所见,这尚未使用IteratorIteratorRecursiveIteratorIterator。相反,它只是使用foreachTraversable接口上运行的那个。

由于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

当与a结合使用时,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的要点中:自己动手:逐行制作工作。


当您继续自定义迭代时,它不会回答提出的问题,包含事实错误,并且遗漏了核心要点。总而言之,这似乎是一种失败的尝试,无法使您悬赏某个您不太了解的主题,或者如果您确实无法将其提炼成对所提出问题的答案。
萨拉

2
好吧,这不能回答我的“为什么”问题,您只是多说几句话而已。也许您实际上从一个重要的错误开始?指向它,不要将其保密。
hakre 2012年

第一句话是不正确的:“ RecursiveIteratorIterator是支持…的IteratorIterator”,这是不正确的。
萨拉

1
@salathe:请提供您的反馈意见。我编辑了答案以解决它。第一句话确实是错误和不完整的。我仍然省略了具体的实现细节,RecursiveIteratorIterator因为它与其他类型共有,但是我确实提供了一些有关其实际工作方式的技术信息。我认为这些示例很好地说明了这些区别:迭代类型两者之间的主要区别。不知道您是否购买了迭代类型,您会以不同的方式来创造硬币,但是恕我直言,迭代类型的语义类型并不容易。
hakre 2012年

1
第一部分得到了一些改进,但是一旦您开始研究示例,仍然会发现事实上的不准确性。如果按水平尺裁剪答案,将会大大改善。
萨拉

31

是什么的差异IteratorIteratorRecursiveIteratorIterator

要了解这两个迭代器之间的区别,必须首先了解所使用的命名约定以及“递归”迭代器的含义。

递归和非递归迭代器

PHP具有非“递归”迭代器,例如ArrayIteratorFilesystemIterator。此外还有“递归”迭代器,如RecursiveArrayIteratorRecursiveDirectoryIterator。后者具有使它们能够被深入的方法,而前者则没有。

当这些迭代器的实例自己遍历时,即使是递归实例,即使循环遍历带有子目录的嵌套数组或目录,这些值也仅来自“最高”级别。

递归迭代(通过递归的行为hasChildren()getChildren()),但不利用它。

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

RecursiveIteratorIterator

这是RecursiveIteratorIterator玩的地方。它具有如何调用“可递归”迭代器的知识,以便以正常,平坦的循环深入到结构中。它将递归行为付诸实践。从本质上讲,它所做的工作是遍历迭代器中的每个值,以查看是否有“子代”要递归或不递归,并步入和退出那些子代集合。您将一个实例粘RecursiveIteratorIterator到一个foreach中,潜入结构中,因此您不必这样做。

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

因此,这是的简要概述RecursiveIteratorIterator,它与IteratorIterator?有什么不同?好吧,您基本上是在问与小猫和树之间的区别什么相同的问题仅仅因为两者都出现在同一个百科全书(或迭代器中的手册)中,并不意味着您应该在两者之间感到困惑。

IteratorIterator

的工作IteratorIterator是获取任何Traversable对象,然后包装它使其满足Iterator接口。这样做的用途是能够在非迭代器对象上应用特定于迭代器的行为。

举一个实际的例子,DatePeriod该类Traversable不是Iterator。这样,我们可以使用循环遍历其值,foreach()但不能执行迭代器通常要做的其他事情,例如过滤。

任务:在接下来的四个星期的星期一,星期三和星期五循环。

是的,foreach在-上DatePeriod使用-并if()在循环内使用in ,这很简单。但这不是这个例子的重点!

$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,因此不仅IteratorIteratorRecursiveIteratorIterator总是“为应用Iterator行为非迭代,Traversable的对象”?(我现在会说foreach通过在容器中的迭代器对象应用迭代类型的对象实现一个迭代器类型的接口等等这些都是迭代器的容器对象,总是Traversable
hakre

如我的回答所述,IteratorIterator是一个类,所有类都将Traversable对象包装在中Iterator没什么了。您似乎在更普遍地使用该术语。
萨拉

看似翔实的答案。一个问题,RecursiveIteratorIterator是否也包装对象,以便它们也可以访问Iterator行为?两者之间的唯一区别是RecursiveIteratorIterator可以向下钻取,而IteratorIterator不能?
Mike Purcell 2012年

@salathe,您知道为什么递归迭代器(RecursiveDirectoryIterator)不实现hasChildren()和getChildren()行为吗?
2012年

7
+1表示“可重复”。这个名称使我误入歧途很长时间,因为Recursivein RecursiveIterator表示行为,而更合适的名称应该是描述能力的名称,例如RecursibleIterator
山羊

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问题

0

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.