如何RecursiveIteratorIterator
工作?
PHP 手册没有太多的文档或解释。IteratorIterator
和 和有什么不一样RecursiveIteratorIterator
?
RecursiveIteratorIterator
是一个具体的Iterator
实现树遍历。它使程序员能够遍历实现RecursiveIterator
接口的容器对象,请参阅Wikipedia 中的 Iterator了解迭代器的一般原则、类型、语义和模式。
与之不同的IteratorIterator
是,具体实现对象遍历以线性顺序(并且默认情况下在其构造函数中Iterator
接受任何类型),允许循环遍历有序对象树中的所有节点,并且其构造函数采用.Traversable
RecursiveIteratorIterator
RecursiveIterator
简而言之:RecursiveIteratorIterator
允许您遍历树,IteratorIterator
允许您遍历列表。我很快就会用下面的一些代码示例来展示这一点。
从技术上讲,这是通过遍历所有节点的子节点(如果有的话)打破线性来实现的。这是可能的,因为根据定义,节点的所有子节点都是 a RecursiveIterator
。然后顶层Iterator
在内部按它们的深度堆叠不同RecursiveIterator
的s,并保持一个指向当前活动子的指针以Iterator
进行遍历。
这允许访问树的所有节点。
基本原理与 with 相同IteratorIterator
:接口指定迭代的类型,基迭代器类是这些语义的实现。与下面的示例相比,对于线性循环,foreach
您通常不会过多考虑实现细节,除非您需要定义一个新的Iterator
(例如,当某些具体类型本身没有实现时Traversable
)。
对于递归遍历-除非您不使用Traversal
已经具有递归遍历迭代的预定义-您通常需要实例化现有RecursiveIteratorIterator
迭代,甚至编写一个Traversable
您自己的递归遍历迭代来进行这种类型的遍历迭代foreach
。
提示:您可能没有自己实现一个或另一个,所以这对于您对它们之间差异的实践经验来说可能是值得做的事情。您会在答案的末尾找到一个 DIY 建议。
简而言之,技术差异:
IteratorIterator
需要任何Traversable
用于线性遍历,但RecursiveIteratorIterator
需要更具体RecursiveIterator
地循环遍历树。IteratorIterator
暴露了它的 main Iterator
via getInnerIerator()
,仅通过该方法RecursiveIteratorIterator
提供当前活动的 sub- 。Iterator
IteratorIterator
完全不知道父母或孩子之类的东西,但RecursiveIteratorIterator
也知道如何获取和遍历孩子。IteratorIterator
不需要迭代器的堆栈,RecursiveIteratorIterator
有这样的堆栈并且知道活动的子迭代器。IteratorIterator
由于线性而没有选择的顺序,可以选择进一步遍历,并且需要RecursiveIteratorIterator
根据每个节点来决定(通过模式 perRecursiveIteratorIterator
决定)。RecursiveIteratorIterator
有比 更多的方法IteratorIterator
。总结一下:RecursiveIterator
是一种具体类型的迭代(在树上循环),它在自己的迭代器上工作,即RecursiveIterator
. 这与 with 的基本原理相同IteratorIerator
,但迭代的类型不同(线性顺序)。
理想情况下,您也可以创建自己的集合。唯一需要的是您的迭代器实现Traversable
了可能通过Iterator
or IteratorAggregate
。然后,您可以将其与foreach
. 例如,某种三叉树遍历递归迭代对象以及容器对象的相应迭代接口。
让我们用一些不那么抽象的现实例子来回顾一下。在接口、具体迭代器、容器对象和迭代语义之间,这可能不是一个坏主意。
以目录列表为例。假设您在磁盘上有以下文件和目录树:
虽然具有线性顺序的迭代器仅遍历顶层文件夹和文件(单个目录列表),但递归迭代器也遍历子文件夹并列出所有文件夹和文件(包含其子目录列表的目录列表):
Non-Recursive Recursive
============= =========
[tree] [tree]
├ dirA ├ dirA
└ fileA │ ├ dirB
│ │ └ fileD
│ ├ fileB
│ └ fileC
└ fileA
您可以轻松地将其与IteratorIterator
不递归遍历目录树进行比较。并且RecursiveIteratorIterator
which 可以遍历到树中,如递归列表所示。
首先是一个非常基本的示例,DirectoryIterator
它实现Traversable
了允许foreach
对其进行迭代:
$path = 'tree';
$dir = new DirectoryIterator($path);
echo "[$path]\n";
foreach ($dir as $file) {
echo " ├ $file\n";
}
上面目录结构的示例输出是:
[tree]
├ .
├ ..
├ dirA
├ fileA
如您所见,这尚未使用IteratorIterator
or 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
. 在第一个示例中它是 aDirectoryIterator
在第二个示例中它是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
当然这不会赢得选美比赛,但它表明使用递归迭代器可以获得更多信息,而不仅仅是key和value的线性顺序。即使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
当您将其与标准遍历进行比较时,所有这些都是不可用的。因此,当您需要将头绕在它周围时,递归迭代会稍微复杂一些,但是它很容易使用,因为它的行为就像迭代器一样,您将其放入 aforeach
并完成。
我认为这些足以作为一个答案的例子。您可以在此要点中找到完整的源代码以及显示漂亮 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
工作。.
IteratorIterator
和有什么区别RecursiveIteratorIterator
?
要了解这两个迭代器之间的区别,首先必须了解所使用的命名约定以及我们所说的“递归”迭代器的含义。
PHP 具有非“递归”迭代器,例如ArrayIterator
and FilesystemIterator
。还有“递归”迭代器,例如RecursiveArrayIterator
and RecursiveDirectoryIterator
。后者具有使它们能够被深入研究的方法,而前者没有。
当这些迭代器的实例自行循环时,即使是递归的,即使循环嵌套数组或带有子目录的目录,这些值也仅来自“顶层”级别。
递归迭代器实现递归行为(通过hasChildren()
, getChildren()
)但不利用它。
将递归迭代器视为“递归”迭代器可能会更好,它们具有递归迭代的能力,但简单地迭代这些类之一的实例不会这样做。要利用递归行为,请继续阅读。
这就是RecursiveIteratorIterator
发挥作用的地方。它知道如何调用“递归”迭代器,以便在正常的、扁平的循环中深入了解结构。它将递归行为付诸行动。它基本上完成了遍历迭代器中的每个值的工作,查看是否有“孩子”可以递归进入或没有,并进入和退出这些孩子的集合。您将一个实例粘贴RecursiveIteratorIterator
到 foreach 中,它会潜入结构中,因此您不必这样做。
如果RecursiveIteratorIterator
未使用 ,您将不得不编写自己的递归循环来利用递归行为,检查“递归”迭代器hasChildren()
并使用getChildren()
.
这是对 的简要概述RecursiveIteratorIterator
,它与 有什么不同IteratorIterator
?好吧,您基本上是在问与小猫和树有什么区别?仅仅因为两者都出现在同一个百科全书(或手册,对于迭代器)中并不意味着您应该在两者之间混淆。
的工作IteratorIterator
是获取任何Traversable
对象,并将其包装以使其满足Iterator
接口。这样做的一个用途是能够在非迭代器对象上应用特定于迭代器的行为。
举一个实际的例子,这个DatePeriod
类是Traversable
但不是一个Iterator
. 因此,我们可以用迭代器循环它的值,foreach()
但不能做我们通常用迭代器做的其他事情,比如过滤。
任务:在接下来的四个星期的星期一、星期三和星期五循环。
是的,通过foreach
-ing和在循环中DatePeriod
使用 an是微不足道的;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
,这就是and之间的区别RecursiveIteratorIterator
。
RecursiveIteraratorIterator
用于迭代RecursiveIterator
(“递归”迭代器),利用可用的递归行为。
IteratorIterator
用于将Iterator
行为应用于非迭代器Traversable
对象。
与 一起使用时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));
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