SPL 中的迭代器详解

caixw

迭代器这种设计模式很常见,也很实用。最著名的要算是 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);
    }

    public 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 中提供的迭代器却实现了对 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 时,它会依次访问对象的公共属性,这是一个非常棒的机制:

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