SPL中的迭代器详解

介绍

迭代器这种设计模式很常见,也很实用。最著名的要算是C++中STL的实现了。它提供了一个统一的接口,使用访问者在不知道类对象内部数据结构的情况下遍历其内部数据。PHP5中提供了对这种设计模式的内置支持,其实所谓的内置支持就是可以使用foreach语言结构来访问实现迭代器接口的类。

一个简单的自定义迭代器

首先看一下下面这三段代码:

$array = array('a', 'b', 'c');
while($a = next($array))
{
    // do something
}
$dh = opendir('/home/test/files');
while(false !== ($file = readdir($dh)))
{
    // do something
}
$fh = fopen("/home/test/files/results.txt", "r");
while(!feof($fh))
{
   $line = fgets($fh);
   // do something
}

以上这三段代码虽然操作的资源(resource)各不相同,但是其功能是一样的,都是遍历资源中的数据。但是这三段代码却用了三套不同的函数,这些函数拥有不同的参数,不同的行为,我们在使用之前必须要了解其用途,才能写出一段遍历某种资源(resource)的代码。

迭代器设计模式就是在这种情况下产生:我们将其中的一些相同的操作抽象出来形成一个接口,不同的资源只要实现这一接口,就能以相同的方式遍历其中的数据。以下是一个简单迭代器接口的实现,在以PHP5之前,迭代器一般也是按照这种方式实现的。当然真实的代码中会比这更加复杂和健壮些。

interface SimpleIterator
{
    public function next();
    public function current();
}
class ArrayIterator implements SimpleIterator
{
    private $_data;
    public function __construct(array $data)
    {    $this->_data = $data;    }
    public function next()
    {    return next($this->_data);    }
    public function current()
    {    return current($this->_data);    }
}
class DirIterator implements SimpleIterator
{
    private $_handle;
    private $_current;
    public function __construct($path)
    {    $this->_handle = opendir($path);    }
    publi function next()
    {
         $this->_current = readdir($this->_handle);
         return false === $this->_current;
    }
    public function current()
    {    return $this->_current;    }
    public function __destruct()
    {    closedir($this->_handle);    }
}
class FileIterator implements SimpleIterator
{
    private $_handle;
    private $_line;
    public function __construct($file, $mode = 'r')
    {    $this->_handle = fopen($file, $mode);    }
    public function next()
    {
         if(feof($this->_handle))
         {    return false;    }
         $this->_line = fgets($this->_handle);
         return true;
    }
    public function current()
    {    return $this->_line;    }
    public function __destruct()
    {    fclose($this->_handle);    }
}
/* 以相同的接口遍历不同资源的数据 */
$arrayIter = new ArrayIterator(array('a','b','c'));
while($arrayIter->next())
{    echo $arrayIter->current();    }
$dirIter = new DirIterator('/home/test/files/')
while($dirIter->next())
{    echo $dirIter->current();    }
$fileIter = new FileIterator('/home/test/fiels/results.txt', 'r');
while($fileIter->next())
{    echo $fileIter->current();    }

SPL中的iterator接口

上面这种自定义实现的迭代器接口有一个缺点,就是不能用于foreach语句。而SPL中提供的iterator却实现了对foreach的支持。

interface Iterator extends Traversable
{
   // 将迭代器的指针移向第一个元素。类似于数组操作函数reset()。
   function rewind();
   // 类似于数组操作函数current()。返回迭代的当前元素。
   function current();
   // 返回当前迭代器元素的键名,类似于数组操作函数key()。
   function key();
   // 将指针移向迭代器的下一个元素,类似于数组操作函数next()。
   function next();
   // 检测在执行了rewind()或是next()函数之后,当前值是否是一个有效的值。
   function valid();
}

这就是SPL的迭代器接口,Traversable是Zend引擎的内置接口,它才是真正让类能用于foreach语句的接口,但是在PHP中并不能直接实现Traversable。只能间接地通过IteratorIteratorAggregate接口实现。下面我们通过两个简单的例子看看如何实现Iterator接口的,虽然有点多余,但有时候代码住住比文字更能说明问题。

class ArrayIterator implements Iterator
{
    private $_data;
    private $_valid;
    public function __construct(array $data)
    {    $this->_data = $data;    }
    public function rewind()
    {    $this->_valid = rewind($this->_data;);    }
    public function current()
    {    return current($this->_data);    }
    public function key()
    {    return key($this->_data);    }
    public function next()
    {    $this->_valid = next($this->_data);    }
    public function valid()
    {    return $this->_valid;    }
}
class DirIterator implements Iterator
{
    private $_handle;
    private $_current;
    public function __construct($dir)
    {    $this->_handle = opendir($dir);    }
    public function rewind()
    {
        rewinddir($this->_handle);
        $this->next();
    }
    public function current()
    {    return $this->_current;    }
    public function key()
    {    return $this->_current;    }
    public function next()
    {    $this->_current = readdir($this->_handle);    }
    public function valid()
    {    return false !== $this->_current;    }
}
$dirIter = new DirIterator('/home/test/files');
foreach($dirIter as $key=>$dir)
{   echo $key,'====>',$dir,'<br />';    }
/* 或者用while的形式 */
$dirIter->rewind();
while($dirIter->valid())
{
    echo $dirIter->key(), '====>', $dirIter->current(), '<br />';
    $dirIter->next();
}

上面这段代码将会输出:

.====>.
..====>..
dir1====>dir1
dir2====>dir2
file2.txt====>file2.txt

相对于while语句,foreach语句隐藏了各函数的调用情况,使人不甚了解其具体调用情况,但是我们只要稍微写点代码就能对其调用情况了如指掌:

class TestIterator implements Iterator
{
    private $_count = 1;
    public function rewind()
    {
        echo 'rewind 
'; $this->_count = 1; } public function current() { echo 'current ', $this->_count, '<br />'; } public function key() { echo 'key ', $this->_count, '<br />'; } public function next() { echo 'next<br />'; $this->_count++; } public function valid() { echo 'valid<br />'; return $this->_count <= 5; } } $test = new TestIterator(); foreach($test as $k=>$v) { // todo; }

不难发现:

  1. foreach在执行前会调用对象的rewind()函数,确保每次都是从头开始。
  2. 之后会调用valid()函数确保值是否有效。
  3. 然后调用key()current()将值赋给$key$dir
  4. 之后执行循环体,然后调用next()进入下一轮循环。

递归迭代器(RecursiveIterator)

这也是一种很常见的迭代器,SPL中也为其定义了一个接口:

interface RecursiveIterator extends Iterator
{
    // 是否存在子元素
    function hasChildren();
    // 获取子元素的迭代器
    function getChildren();
}

我们依旧以上面的DirIterator类为例,实现一个递归的迭代器。因为RecursiveIterator也是从Iterator继承而来的,所以我们的递归类也不用从头开始写,只须从DirIterator继承再实现RecursiveIterator接口的两个特有函数即可:

class RecursiveDirIterator extends DirIterator implements RecursiveIterator
{
    private $_path;
    public function __construct($path)
    {
        parent::__construct($path);
        $this->_path = $path;
    }
    public function hasChildren()
    {
        $c = $this->current();
        // 需要过滤掉 '.'和'..'目录
        return (is_dir($this->_path . DIRECTORY_SEPARATOR . $c) && $c != '.' && $c !='..');
    }
    public function getChildren()
    {
        return new RecursiveDirIterator($this->_path . DIRECTORY_SEPARATOR . $this->current());
    }
}
$rdi = new RecursiveDirIterator('/home/test/files');
foreach($rdi as $k=>$v)
{   echo $k,'===>',$v,'
'; }

IteratorAggergate接口

IteratorAggergate是除Iterator之外另一个从Traversable接口中继承而来的。其接口也很简单,只有一个函数。就是返回一个迭代器实例:

interface IteratorAggergate extends Traversable
{
    public function getIterator();
}

该接口的功能也是让实现者拥有迭代器的功能。初看之下,貌似没什么特别的,我们完全可以用Iterator接口来替代。事实上也是如此。之所以提供该类只不过是让我们少写点代码和减少类与类之间的耦合度。

我们依旧以DirIterator为例。假设有一个类A包含了一个私有成员DirIterator,而类A本身又要实现DirIterator的迭代器功能。按照我们之前的作法,会让类A实现Iterator接口,然后在各接口函数重写一次DirIterator的内容。但是这样做,若是后期将DirIterator更改为RecursiveDirIterator,则同时需要更改类A的接口,以符合要求。而用IteratorAggergate则不会出现这种情况。

class A implements IteratorAggergate
{
    private $_dirIter;
    public function __construct()
    {   $this->_dirIter = new DirIterator('/home/test/files');    }
    // 返回一个迭代。
    public function getIterator()
    {    return $this->_dirIter;    }
}
$a = new A();
$iter = $a->getIterator();
while($iter->valid())
{
    // do something
}
// foreach 能识别IteratorAggergate接口,并取得迭代器,进入循环。
foreach($a as $v)
{
    // do something
}

OuterIterator

interface OuterIterator extends Iterator
{
    public function getInnerIterator();
}

OuterIterator相当于我们前一节讲的类A的另一种实现。它实现者可以包含一个或多个迭代器成员,即可以通过getInnerIterator()接口函数获取内部的迭代器,也可以直接通过类本身实现的Iterator接口遍历内部的迭代器数据。这在SPL是一个非常重要的接口,SPL中很多内置的迭代器实现了这个接口。

FilterIterator

FilterIterator这是一个抽象类,它实现了OuterIterator接口。它包装一个已有的迭代器类,通过抽象方法accept()过滤掉不需要的内容,形成一个新的迭代器。我们还是用上面的DirIterator作一个例子,定义一个只返回所有以'a'开头的目录名的迭代器:

class ADirIterator extends FilterIterator
{
    public function accept()
    {
        return 0 === strpos($this->current(), 'a');
    }
}
$dirIter = new DirIter('/home/test/files');
$adirIter = new ADirIter($dirIter);
foreach($adirITer as $dir)
{    /* todo */    }

LimitIterator

这也是一个实现OuterIterator的类。它有点类似于SQL中的LIMIT语句。它通过包装一个已有迭代器,然后截取其中某一段数据形成一个新的迭代器。它同时还提供了两个函数:
getPosition():当前迭代器的位置。
seek($pos):直接跳到某个位置的元素。

$data = array('a','d','c','f','g');
$offset = 2;
$count = 3;
$limitData = new LimitIterator(new ArrayIterator($data), $offset, $count);
foreach($limitData as $v)
{
    echo $limitData->getPosition(), ':', $v, '<br />';
}
try{
     $limitData->seek(4);
}catch(exception $e)
{    echo $e->getMessage();    }

遍历对象属性

当我们对一个没有实现iterator接口的对象使用foreach时,它会依次访问对象的公共(public)属性,这是一个非常棒的机制:

class Test
{
    public $p1 = 1;
    public $p2 = 2;
    protected $p3 = 3;
    protected $p4 = 4;
    private $p5 = 5;
    private $p6 = 6;
}
$t = new Test();
foreach($t as $property=>$value)
{    echo $property,'====>',$value,'<br />';    }

上例中会显示出$p1,$p2的属性。而$p3,$p4,$p5,$p6则因为访问权限问题无法列出。若想要列出被保护的成员,只需将foreach移到类内即可:

class Test
{
    public $p1 = 1;
    public $p2 = 2;
    protected $p3 = 3;
    protected $p4 = 4;
    private $p5 = 5;
    private $p6 = 6;
    public function properties()
    {
        foreach($this as $property=>$value)
        {    echo $property,'====>',$value,'<br />';    }
    }
}

SPL中一些已实现的迭代器类

在SPL中已经定义了一非常有用的迭代器类,我们可以直接拿来用:

  • DirectoyIterator: 和我们上面实现的DirIterator类大同小异。但是功能更全
  • RecursiveDirectoryIterator:依旧和我们上面实现的RecursiveDirIterator类很相似。
  • SimpleXMLIterator:一个遍历XML内容的类,关于它的信息网站信息多得不得了。这里也不做介绍了。但使用它的时候有一点需要注意,具体情况看这里
  • IteratorIterator:实现对迭代器的包装,这也是SPL中对OuterIterator默认实现。
  • NoRewindIterator:取消了rewind()函数的迭代器。
  • InfiniteIterator:从字面意思就知道,这是个无限循环的迭代器,当next()到达最后时,会自动调用rewind()函数,又从头开始。
  • AppendIterator:它实现了对一系统迭代器的包装,并且可以在运行过程中添加新的迭代器。
  • SplFileObject:文件操作类,可以按行的方式遍历文件内容。同时还能获取文件的大小及其它详细信息。

参考

Introducing PHP 5's Standard Library

Iterators in PHP5

本文采用 知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议 进行许可。
转载时请注明出处:https://caixw.io/posts/2010/spl-iterator.html

posted @ 2010-07-28 03:53  caixw  阅读(500)  评论(0编辑  收藏  举报