【问题标题】:How does RecursiveIteratorIterator work in PHP?RecursiveIteratorIterator 在 PHP 中是如何工作的?
【发布时间】:2012-08-18 02:46:51
【问题描述】:

RecursiveIteratorIterator 是如何工作的?

PHP 手册没有太多的文档或解释。 IteratorIteratorRecursiveIteratorIterator有什么区别?

【问题讨论】:

  • php.net/manual/en/recursiveiteratoriterator.construct.php 有一个例子,php.net/manual/en/class.iteratoriterator.php 也有一个介绍 - 你能指出你到底在理解什么方面有困难吗?手册应包含哪些内容才能更容易掌握?
  • 如果您问RecursiveIteratorIterator 的工作原理,您是否已经了解IteratorIterator 的工作原理?我的意思是基本一样,只是两者消耗的接口不同。您是对一些示例更感兴趣,还是想看看底层 C 代码实现的差异?
  • @Gordon 我不确定单个 foreach 循环如何遍历树结构中的所有元素
  • @hakra 我现在正在尝试研究所有内置接口以及 spl 接口和迭代器实现。我很想知道它在 forach 循环的背景下如何工作,并举了一些例子。
  • @hakre 它们实际上都非常不同。 IteratorIteratorIteratorIteratorAggregate 映射为 Iterator,其中 REcusiveIteratorIterator 用于反复遍历 RecursiveIterator

标签: php iterator spl


【解决方案1】:

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() 公开其主IteratorRecursiveIteratorIterator 仅通过该方法提供当前活动的子Iterator
  • 虽然IteratorIterator 完全不知道诸如父母或孩子之类的东西,但RecursiveIteratorIterator 也知道如何获取和遍历孩子。
  • IteratorIterator 不需要迭代器堆栈,RecursiveIteratorIterator 有这样的堆栈并且知道活动的子迭代器。
  • IteratorIterator 由于线性而有其顺序且没有选择,RecursiveIteratorIterator 有进一步遍历的选择,需要根据每个节点进行决定(通过 mode per RecursiveIteratorIterator 决定)。
  • RecursiveIteratorIterator 的方法比 IteratorIterator 多。

总结一下:RecursiveIterator 是一种具体类型的迭代(在树上循环),它在自己的迭代器上工作,即RecursiveIterator。这与IteratorIerator 的基本原理相同,但迭代的类型不同(线性顺序)。

理想情况下,您也可以创建自己的集合。唯一需要的是您的迭代器实现Traversable,这可以通过IteratorIteratorAggregate 实现。然后您可以将它与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。相反,它只是使用在Traversable 接口上运行的foreach

由于foreach 默认情况下只知道名为线性顺序的迭代类型,我们可能希望明确指定迭代的类型。乍一看,它可能看起来太冗长,但出于演示目的(并且为了让RecursiveIteratorIterator 的区别在以后更明显),让我们指定迭代的线性类型,明确指定目录列表的IteratorIterator 迭代类型:

$files = new IteratorIterator($dir);

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

本示例几乎与第一个示例相同,不同之处在于 $files 现在是 Traversable $dirIteratorIterator 迭代类型:

$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

当然这不会赢得选美比赛,但它表明使用递归迭代器可以获得更多信息,而不仅仅是 keyvalue 的线性顺序。即使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

【讨论】:

  • 当您继续自定义迭代时,它不回答提出的问题,包含事实错误并错过核心点。总而言之,在一个你不太了解的主题上获得赏金似乎是一次糟糕的尝试,或者,如果你知道,也无法提炼出所提出问题的答案。
  • 好吧,这并没有回答我的“为什么”问题,你只是多说几句而已。也许你从一个真正重要的错误开始?指向它,不要保密。
  • 第一句话是不正确的:“RecursiveIteratorIterator is an IteratorIterator that supports…”,这不是真的。
  • @salathe:请为您提供反馈。我编辑了答案以解决它。第一句话确实是错误的和不完整的。我仍然忽略了RecursiveIteratorIterator 的具体实现细节,因为这与其他类型相同,但我确实提供了一些关于它实际工作方式的技术信息。我认为这些例子很好地显示了差异:迭代的类型两者之间的主要区别。不知道你是否购买了迭代类型,但你创造的迭代类型略有不同,但恕我直言,迭代的语义类型并不容易。
  • 第一部分有所改进,但是一旦你开始研究示例,它仍然会转向事实不准确。如果你在水平规则处裁剪答案,它会大大改善。
【解决方案2】:

IteratorIteratorRecursiveIteratorIterator有什么区别?

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

递归和非递归迭代器

PHP 具有非“递归”迭代器,例如 ArrayIteratorFilesystemIterator。还有“递归”迭代器,例如RecursiveArrayIteratorRecursiveDirectoryIterator。后者有方法可以深入研究,而前者没有。

当这些迭代器的实例被自己循环遍历时,即使是递归的,即使循环嵌套数组或带有子目录的目录,值也只会来自“顶层”。

递归迭代器实现递归行为(通过hasChildren()getChildren())但不要利用它。

将递归迭代器视为“可递归”迭代器可能会更好,它们具有能力进行递归迭代,但简单地迭代这些类之一的实例不会这样做。要利用递归行为,请继续阅读。

递归迭代器迭代器

这就是RecursiveIteratorIterator 发挥作用的地方。它知道如何调用“递归”迭代器,以便在正常的、扁平的循环中深入了解结构。它将递归行为付诸行动。它基本上完成了遍历迭代器中的每个值的工作,查看是否有“孩子”要递归进入或没有,以及进入和退出这些孩子的集合。您将RecursiveIteratorIterator 的实例粘贴到foreach 中, 会潜入结构中,这样您就不必这样做了。

如果不使用RecursiveIteratorIterator,您将不得不编写自己的递归循环来利用递归行为,检查“递归”迭代器的hasChildren() 并使用getChildren()

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

迭代器迭代器

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

举一个实际的例子,DatePeriod 类是Traversable,但不是Iterator。因此,我们可以使用 foreach() 循环其值,但不能执行我们通常使用迭代器执行的其他操作,例如过滤。

任务:循环遍历接下来四个星期的周一、周三和周五。

是的,foreach-ing 覆盖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) { … }

上面的 sn-p 不起作用,因为 CallbackFilterIterator 需要一个实现 Iterator 接口的类的实例,而 DatePeriod 没有。但是,由于它是Traversable,我们可以通过使用IteratorIterator 轻松满足该要求。

$period = new IteratorIterator(new DatePeriod(…));

如您所见,这与迭代迭代器类或递归无关,这就是IteratorIteratorRecursiveIteratorIterator 之间的区别。

总结

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

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

【讨论】:

  • IteratorIterator 不只是Traversable 对象的线性顺序遍历的标准类型吗?那些可以在没有它的情况下仅在foreach 中使用的?更进一步,RecursiveIterator always 不是 Traversable,因此不仅IteratorIterator 而且RecursiveIteratorIterator always "用于将Iterator 行为应用于非迭代器, 可遍历对象”? (我现在说foreach 通过迭代器对象在实现迭代器类型接口的容器对象上应用迭代类型,因此这些是迭代器容器对象,总是Traversable
  • 正如我的回答所说,IteratorIterator 是一个将Traversable 对象包装在Iterator 中的类。 仅此而已。您似乎更广泛地应用了该术语。
  • 看似内容丰富的答案。一个问题,RecursiveIteratorIterator 不会也包装对象,以便它们也可以访问 Iterator 行为吗?两者之间的唯一区别是 RecursiveIteratorIterator 可以向下钻取,而 IteratorIterator 不能?
  • @salathe,你知道为什么递归迭代器(RecursiveDirectoryIterator)没有实现hasChildren()、getChildren()行为吗?
  • +1 表示“可递归”。这个名字让我误入歧途很长一段时间,因为RecursiveIterator 中的Recursive 暗示了行为,而更合适的名字应该是描述能力的名字,比如RecursibleIterator
【解决方案3】:

当与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 遍历其内部迭代器的方式的一个属性。
【解决方案4】:

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

【讨论】:

    猜你喜欢
    • 2013-01-18
    • 1970-01-01
    • 2011-01-23
    • 2010-10-05
    • 1970-01-01
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    • 2013-08-24
    相关资源
    最近更新 更多