点击上方关注Lemon黄,么么哒!
听说
与魔鬼战斗的人,应当小心自己不要成为魔鬼。当你远远凝视深渊时,深渊也在凝视你。
——尼采
来源:https://www.startutorial.com/articles/view/modern-php-developer-iterator
译/Lemon黄
如果在PHP中使用过for循环,那么迭代的思想对我们而言并不陌生。将数组传递给for循环,并在循环内执行一些逻辑,但是你知道实际上可以将数组以外的数据结构传递给for循环吗?这就是迭代器(Iterator)可以发挥作用的地方。
1、Iterator的定义
以下是Wikipedia(维基百科)中对迭代器的摘要定义:
在计算机编程中,迭代器是使程序员能够遍历容器(尤其是列表)的对象。请注意,迭代器执行遍历并且还可以访问容器中的数据元素,但不执行迭代。 迭代器在行为上类似于数据库游标。
这里要记住一些关键点:
现在我们知道了Iterator(迭代器,下文不再做翻译)的定义,这个概念可能仍然有些晦涩,但是不用担心,我们还没有讲完。现在,我们已经知道了Iterator的工作原理类似于array,并且可以在for循环中进行遍历。
了解数组在for循环中的实际工作方式将对我们很有帮助。让我们看一下下面的代码:
$data = array(1,2,3,4);
for ($i=0; $i<count($data); $i++) {
$key = $i;
$value = $data[$i];
}
从上我们可以知道数组在for循环中的工作方式:
我们可以将这些步骤抽象为简单的函数,如下所示:
在抽象级别上,我们可以想象,只要一个对象提供上述五个功能,就可以通过for循环遍历它。
实际上,迭代器不过是一个类,它实现了上面提到的所有五个步骤。在PHP中,标准PHP库(SPL)是旨在解决常见问题的接口和类的集合,它提供了标准的Iterator接口。
Iterator extends Traversable {
/* Methods */
abstract public mixed current ( void )
abstract public scalar key ( void )
abstract public void next ( void )
abstract public void rewind ( void )
abstract public boolean valid ( void )
}
2、自定义迭代器类
现在我们了解了迭代器是什么,是时候我们可以自己构建一个迭代器了。
我们的第一个迭代器代表了来自Github上的十大最受关注的PHP存储库。我们可以将其传递给foreach并像数组一样遍历它。我们将其命名为TrendingRepositoriesIterator。
首先,我们需要使我们的类实现Iterator接口。
class TrendingRepositoriesIterator implements Iterator
{
public function rewind()
{
}
public function valid()
{
}
public function next()
{
}
public function key()
{
}
public function current()
{
}
}
迭代器必须始终实现上述五个方法。TrendingRepositoriesIterator类的最终代码如下:
class TrendingRepositoriesIterator implements Iterator
{
private $repos = [];
private $pointer = 0;
public function __construct()
{
$this->populate();
}
public function rewind()
{
$this->pointer = 0;
}
public function valid()
{
return isset($this->repos[$this->pointer]);
}
public function next()
{
$this->pointer++;
}
public function key()
{
return $this->pointer;
}
public function current()
{
return $this->repos[$this->pointer];
}
private function populate()
{
$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://api.github.com/search/repositories', [ 'query' => ['q' => 'language:php', 'sort' => 'stars', 'order' => 'desc']]);
$resInArray = json_decode($res->getBody(), true);
$trendingRepos = array_slice($resInArray['items'], 0, 10);
foreach ($trendingRepos as $rep) {
$this->repos[] = $rep['name'];
}
}
}
让我们看一下TrendingRepositoriesIterator的用例,它可以像数组一样使用:
$trendingRepositoriesIterator = new TrendingRepositoriesIterator();
foreach ($trendingRepositoriesIterator as $repository) {
echo $repository . "\n";
}
// 输出
laravel
symfony
CodeIgniter
DesignPatternsPHP
Faker
yii2
composer
WordPress
sage
cakephp
太棒了!现在,我们已经编写了第一个迭代器,正如你所看到的,它实际上非常容易和直接。
3、为什么要使用迭代器?
可能你仍然想知道为什么我们需要使用迭代器。我们不能只使用数组吗?答案是肯定的。在大多数情况下,虽然迭代器确实具有一些关键优势,但数组将足以胜任这项工作,我们将在后面分享这些优势。请记住,我们绝不建议在任何情况下都使用迭代器。
3.1、封装形式
在我们的第一个迭代器TrendingRepositoriesIterator中,遍历Github存储库的详细信息从外部获取,在内部隐藏完成。我们可以更新如何获取数据,从何处获取数据以及如何遍历资源。客户端代码无需更改。这就是所谓的封装,是面向对象编程的关键概念之一。
其他示例包括:
要遍历MySQl结果,我们可以使用:
$result = mysql_query("SELECT * FROM books");
// Iterate over the structure
while ( $row = mysql_fetch_array($result) ) {
// do stuff
}
要遍历文本文件的内容,我们可以:
$fh = fopen("books.txt", "r");
// Iterate over the structure
while (!feof($fh)) {
$line = fgets($fh);
// do stuff with the line here
}
使用迭代器,我们可以封装遍历资源的过程,以便外部世界不了解内部操作。实际上,外界不需要知道我们从何处获取数据或如何以循环方式遍历数据。他们需要知道的是,他们可以像下面这样简单地进行迭代:
$bookIterator = new BookIterator();
foreach($bookIterator as $book) {
// do stuff with $book
}
封装是一个非常强大的概念,它使我们能够编写简洁的代码。
3.2、高效的内存使用
有效的内存使用是迭代器的主要优势。
在我们的TrendingRepositoriesIterator类中,我们实际上可以动态地获取资源,这意味着仅当调用next()方法时,才从Github API获取数据。这种技术被称为懒加载。它仅在需要时才生成值,因此可以帮助我们节省大量内存。
3.3、易于添加其他功能
使用迭代器的另一个好处是我们可以装饰它以添加其他功能。以我们的TrendingRepositoriesIterator类为例。我们想从资源中排除“ laravel”。一种明显的方法是更新我们的原始类,尽管这当然不是我们在此要做的。
我们可以使用SPL的CallbackFilterIterator装饰原始的迭代器,而TrendingRepositoriesIterator完全不需要更改。
$trendingRepositoriesIterator = new TrendingRepositoriesIterator();
return $value != 'laravel';
});
foreach ($newTrendingRepositoriesIterator as $repository) {
echo $repository . "\n";
}
// 输出
symfony
CodeIgniter
DesignPatternsPHP
Faker
yii2
composer
WordPress
sage
cakephp
最有意义的部分是没有对象的重复。仅当TrendingRepositoriesIterator命中next()方法时,才会触发该回调,然后将相应地应用该逻辑。这是节省内存和提高性能的好方法。
4、SPL迭代器
既然我们了解了使用迭代器的强大功能和好处,那么使用迭代器解决合适的问题是一个很好的实践。但是,如果在遇到新问题时都要我们自己编写迭代器,则这将非常耗时,因为它确实需要我们实现一组预定义的函数。
幸运的是,PHP在提供了一组迭代器以解决一些常见问题方面做得很好。在以下各节中,我们将研究SPL提供的一组通用迭代器。再回顾一下,标准PHP库的SPL标准旨在提供一组接口和类,以解决常见问题。
5、ArrayObject与SPL ArrayIterator
在PHP中,数组是八种基本类型之一。PHP提供了79个函数来处理与数组相关的任务(参考)。使用数组是完全合适的,但是有时我们可能希望将数组用作对象,这具体取决于我们对面向对象编程的了解。在这种情况下,PHP提供了两个类来使数组成为面向对象代码中的一等公民。
5.1、ArrayObject
第一个我们可以选择的类是ArrayObject类。此类允许对象作为数组操作。
让我们看一下它的类签名:
ArrayObject implements IteratorAggregate , ArrayAccess , Serializable , Countable{
...
public ArrayIterator getIterator ( void )
...
}
如上所述,ArrayObject实现了IteratorAggregate。 那么什么是IteratorAggregate呢?它是创建外部迭代器的接口。简而言之,它是创建迭代器的快速方法,而不是使用五个方法(rewind,valid,current,key and valu)实现Iterator接口,IteratorAggregate允许你将该任务委托给另一个迭代器。你需要做的就是实现一个方法getIterator()。
IteratorAggregate extends Traversable {
abstract public Traversable getIterator ( void )
}
ArrayObject实现IteratorAggregate。它为迭代器功能创建一个外部ArrayIterator。
当ArrayObject实现IteratorAggregate时,我们可以像数组一样在foreach循环中使用它。
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayObject = new ArrayObject($books);
foreach ($booksAsArrayObject as $book) {
echo $book . "\n";
}
// 输出
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
我们要使用ArrayObject的主要原因是可以以面向对象的方式来使用数组。
books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayObject->append('The Pragmatic Programmer: From Journeyman to Master'); // --- vs ---
$books[] = 'The Pragmatic Programmer: From Journeyman to Master';
5.2、ArrayIterator
ArrayIterator的工作方式类似于ArrayObject。
让我们看看它的类签名:
ArrayIterator implements ArrayAccess , SeekableIterator , Countable , Serializable {
}
就它们实现的接口而言,它几乎与ArrayObject相同。唯一的区别是,它不是ArrayObject实现的ArrayIterator接口,而是实现了SeekableIterator。
我们使用ArrayIterator的方式与在foreach循环中使用ArrayObject的方式相同:
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayIterator = new ArrayIterator($books);
foreach ($booksAsArrayIterator as $book) {
echo $book . "\n";
}
// Output
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
以面向对象的方式使用数组:
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayIterator = new ArrayIterator($books);
$booksAsArrayIterator->append('The Pragmatic Programmer: From Journeyman to Master'); // --- vs ---
$books[] = 'The Pragmatic Programmer: From Journeyman to Master';
5.3、Comparison
您可能想知道何时使用ArrayObject和何时使用ArrayIterator。重要的是要了解ArrayObject和ArrayIterator之间的区别和关系。
正如我们在ArrayObject部分中已经发现的那样,ArrayObject实际上将ArrayIterator创建为外部迭代器。可以说ArrayIterator做了ArrayObject的工作,并且它提供了更多的功能,特别是寻找位置。这是通过实现SeekableIterator来完成的。
除了将指针作为迭代器从上到下移动之外,它还允许随机跳转到某个位置。
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsArrayIterator = new ArrayIterator($books);
$booksAsArrayIterator->seek(3);
echo $booksAsArrayIterator->current();
// 输出
Agile Software Development, Principles, Patterns, and Practices
最后,ArrayIterator是SPL的一部分,而ArrayObject不是。
6、迭代文件系统
列出给定目录的内容是一项非常常见的任务。PHP提供了许多用于处理文件系统的功能。其中之一是scandir()。
假设给我们一个任务,以列出给定目录中的所有文件,如下所示:
---books
| ---book_item_1.txt
| ---book_item_2.txt
| ---book_item_3.txt
| ---book_item_4.txt
我们可以通过scandir()完成它,如下所示:
$books = scandir("books");
foreach($books as $book) {
echo $book . "\n";
}
// 输出
.
..
book_item_1.txt
book_item_2.txt
book_item_3.txt
book_item_4.txt
这是两个虚拟目录(“.”和“ ..”),您可以在文件系统的每个目录中找到它们。
由于本节是关于迭代器的,因此我们将介绍一些用于处理文件系统的迭代器。希望在您的下一个项目中,您将能够利用其中的一些。三个方便的迭代器派上用场:DirectoryIterator,FilesystemIterator和RecursiveDirectoryIterator。
在研究它们中的每一个之前,先了解一下它们的继承关系是很有用的:
DirectoryIterator extends SplFileInfo
FilesystemIterator extends DirectoryIterato
RecursiveDirectoryIterator extends FilesystemIterator
6.1、DirectoryIterator
DirectoryIterator类提供了一个用于查看文件系统目录内容的简单接口。
为了完成相同的任务,我们可以使用DirectoryIterator:
$books = new DirectoryIterator('books');
foreach($books as $book) {
echo $book->getFilename() . "\n";
}
// 输出
.
..
book_item_1.txt
book_item_2.txt
book_item_3.txt
book_item_4.txt
创建DirectoryIterator对象所需的唯一参数是目录的路径。与scandir函数相比,DirectoryIterator返回一个对象,而不是文件名作为字符串。该对象包含与文件有关的各种信息,我们可以使用这些信息。
6.2、FilesystemIterator
要通过使用FilesystemIterator完成相同的任务,我们可以使用:
$books = new FilesystemIterator('books');
foreach($books as $book) {
echo $book->getFilename() . "\n";
}
//输出
book_item_1.txt
book_item_2.txt
book_item_3.txt
book_item_4.txt
它看起来与DirectoryIterator几乎相同,除了FilesystemIterator自动过滤出两个虚拟目录。
他们真的一样吗?我们可以使用一种简单的方法来区分这些差异:
$books = new DirectoryIterator('books');
foreach($books as $key=>$value) {
echo $key . ' is a type of '. gettype($key) . "\n";
echo $value . ' is a type of '. get_class($value) . "\n";
}
echo '-------------------------'."\n";
$books = new FilesystemIterator('books');
foreach($books as $key=>$value) {
echo $key . ' is a type of '. gettype($key) . "\n";
echo $value . ' is a type of '. get_class($value) . "\n";
}
从CLI运行上述脚本的结果是:
0 is a type of integer
. is a type of DirectoryIterator
1 is a type of integer
.. is a type of DirectoryIterator
2 is a type of integer
book_item_1.txt is a type of DirectoryIterator
3 is a type of integer
book_item_2.txt is a type of DirectoryIterator
4 is a type of integer
book_item_3.txt is a type of DirectoryIterator
5 is a type of integer
book_item_4.txt is a type of DirectoryIterator
--------------------------------
books/book_item_1.txt is a type of string
books/book_item_1.txt is a type of SplFileInfo
books/book_item_2.txt is a type of string
books/book_item_2.txt is a type of SplFileInfo
books/book_item_3.txt is a type of string
books/book_item_3.txt is a type of SplFileInfo
books/book_item_4.txt is a type of string
books/book_item_4.txt is a type of SplFileInfo
现在我们可以看到它们在内部实际上是完全不同的:
实际上,FilesystemIterator具有更多的灵活性。创建FilesystemIterator对象时,它类似于DirectoryIterator接受目录路径作为第一个参数。此外,您可以选择将第二个参数作为标志传递。该标志能够配置此功能的各个方面。
7、展望CachingIterator
在本节中,我们将介绍一个迭代器,该迭代器可以窥视迭代中的下一个元素。此功能使我们能够做很多有用的事情,例如在迭代器到达列表末尾时执行不同的操作。
具有这种强大功能的类是CachingIterator。
首先让我们看一下它的类签名,然后,我们将详细介绍它的用法。
CachingIterator extends IteratorIterator
CachingIterator继承自IteratorIterator。 什么是IteratorIterator? 它只是引擎盖下另一个迭代器的包装。 它将把五个Itertator方法(rewind(),current(),key(),valid(),next())调用转发给它所环绕的迭代器。 我们还可以通过调用方法getInnerIterator()来检索内部迭代器。
由于此类的性质,内部迭代器的指针总是比CachingIterator向前移动一步,并且CachingIterator提供了一个hasNext()方法来告诉我们它是否到达列表的末尾。 这就是CachingIterator向前看的方式。
现在,让我们来实践一下。
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsCachingIterator = new CachingIterator(new ArrayIterator($books));
foreach ($booksAsCachingIterator as $book) {
echo 'current book - ' . $book . PHP_EOL;
if ($booksAsCachingIterator->hasNext()) {
echo '----------------------------' . PHP_EOL;
}
}
在CLI中运行上述脚本的结果:
current book - Head First Design Patterns
next book - Clean Code: A Handbook of Agile Software Craftsmanship
----------------------------
current book - Clean Code: A Handbook of Agile Software Craftsmanship
next book - Domain-Driven Design: Tackling Complexity in the Heart of Software
----------------------------
current book - Domain-Driven Design: Tackling Complexity in the Heart of Software
next book - Agile Software Development, Principles, Patterns, and Practices
----------------------------
current book - Agile Software Development, Principles, Patterns, and Practices
与其他迭代器类似,要创建CachingIterator实例,我们将迭代器作为第一个参数传递给类承包商。正如我们所看到的,向前偷看的真正魔力是由hasNext()方法提供的。 该方法可以告诉我们是否存在下一个立即元素。
除了第一个参数之外,CachingIterator还可以选择接受第二个参数作为标志。
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsCachingIterator = new CachingIterator(new ArrayIterator($books), CachingIterator::TOSTRING_USE_KEY;
foreach ($booksAsCachingIterator as $key=>$book) {
echo $booksAsCachingIterator . PHP_EOL;
}
// 输出 0
1
2
3
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
$booksAsCachingIterator = new CachingIterator(new ArrayIterator($books), CachingIterator::TOSTRING_USE_CURRENT);
foreach ($booksAsCachingIterator as $key=>$book) {
echo $booksAsCachingIterator . PHP_EOL;
}
// 输出
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
8、 生成器Generator
现在,我们对迭代器的好处深信不疑。它们封装了遍历的详细信息,并且比创建内存数组要有效得多。但是,一切都有其代价。要创建迭代器,我们仍然必须实现SPL Iterator接口。您可能对迭代器感到恐惧,并且不想实现Iterator接口所约定的这五个方法。实施它们非常耗时,有时甚至很复杂。
从PHP 5.5开始,我们将不会再受到这个困扰。 PHP引入了一些生成器,它们提供了一种简单的方法来实现简单的迭代器,而又不会增加实现迭代器接口的类的开销或复杂性。
究竟是什么生成器? 生成器类似于普通的PHP函数,不同之处在于它具有特殊的关键字“ yield”。
以下是生成器功能的简单示例。 在实际的应用程序中,我们将不会有这样的生成器-此处仅用于演示:
function booksGenerator()
{
$books = array(
'Head First Design Patterns',
'Clean Code: A Handbook of Agile Software Craftsmanship',
'Domain-Driven Design: Tackling Complexity in the Heart of Software',
'Agile Software Development, Principles, Patterns, and Practices',
);
foreach ($books as $book) {
yield $book;
}
}
foreach (booksGenerator() as $book) {
echo $book . PHP_EOL;
}
// 输出
Head First Design Patterns
Clean Code: A Handbook of Agile Software Craftsmanship
Domain-Driven Design: Tackling Complexity in the Heart of Software
Agile Software Development, Principles, Patterns, and Practices
当内部发现yield关键字时,PHP在内部实现了生成器功能。首次调用生成器函数时,PHP将创建一个Generator对象。这个Generator对象是内部类Generator的一个实例,并且Generator类实现Iterator接口。这样,用户就可以创建迭代器而无需编写合同规定的代码,这一切都要归功于PHP Generator。
当我们需要提供步长值时,将调用yield。 将其视为常规迭代器中函数或当前方法的返回。
让我们将第一个迭代器类TrendingRepositoriesIterator中的一个转换为生成器函数:
function trendingRepositoriesGenerator()
{
$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://api.github.com/search/repositories', [ 'query' => ['q' => 'language:php', 'sort' => 'stars', 'order' => 'desc'] ]);
$resInArray = json_decode($res->getBody(), true);
$trendingRepos = array_slice($resInArray['items'], 0, 10);
foreach ($trendingRepos as $rep) {
yield $rep['name'];
};
}
事实证明,使用生成器的代码要少得多。 我们也可以像使用TrendingRepositoriesIterator一样,在foreach循环中使用它:
foreach (trendingRepositoriesGenerator() as $repo) {
echo $repo . PHP_EOL;
}
请注意,生成器本身并没有提供任何特殊功能,它们只是使创建迭代器更加简单。 换句话说,它们绝对不是迭代器的替代品。