PHP的SPL扩展库(三)迭代器

硬核项目经理
English

关于迭代器,我们在之前设计模式相关的文章中已经讲过迭代器具体是个啥,而且也使用过 SPL 的例子来演示过,要是没有看过之前的文章的可以穿越回去看一下哦!PHP设计模式之迭代器模式https://mp.weixin.qq.com/s/uycac0OXYYjAG1BlzTUjsw

因此,对于迭代器的概念,我们这里就不会多说了,今天的主要内容就是来了解一下 SPL 扩展中都包含哪些迭代器以及它们的功能效果。另外,上一篇文章中我们接触过的数组迭代器 ArrayIterator 由于已经学习过了,也就不放在这里讲了。此外还有文件目录相关的迭代器,也会放在和其相关的文件目录操作的文章中讲解,包括下面学习的这些迭代器还有不少都有相对应的 递归式 的迭代器,比如我们下面要讲到的 CachingIterator 、 FilterIterator 等等,都有它们对应的 RecursiveCachingIterator 、 RecursiveFilterIterator 类,这个大家就自己去研究下吧,带递归迭代器,也就是多了两个方法 getChildren() 和 hasChildren() 而已,最后我们还会实现一个自己的迭代器类,其中就会讲到递归这块。

IteratorIterator 包装迭代器

首先我们来看一下什么是包装迭代器。它本身也是个迭代器,但是在实例化的时候,又必须给他再传递进来一个迭代器并保存在内部,是一个内部迭代器 InnerIterator 。对于它自身的那些迭代器接口函数来说,其实都是转发调用的那个内部迭代器相关的操作函数。感觉上其实就有点像是一个装饰器模式,我们可以通过继承 IteratorIterator 来实现对原有迭代器功能的升级。

$iterator = new IteratorIterator(new ArrayIterator([1, 2, 3]));
$iterator->rewind();
while ($iterator->valid()) {
    echo $iterator->key(), ": ", $iterator->current(), PHP_EOL;
    $iterator->next();
}
// 0: 1
// 1: 2
// 2: 3

从代码中可以看出,它的构造参数必须还得是一个迭代器,本身的参数签名就是需要一个实现了 Traversable 接口的对象。Traversable 接口是所有迭代器所必须要实现的接口。

class OutIterator extends IteratorIterator
{
    public function rewind()
    {
        echo __METHOD__, PHP_EOL;
        return parent::rewind();
    }

    public function valid()
    {
        echo __METHOD__, PHP_EOL;
        return parent::valid();
    }

    public function current()
    {
        echo __METHOD__, PHP_EOL;
        return parent::current() . '_suffix';
    }

    public function key()
    {
        echo __METHOD__, PHP_EOL;
        return parent::key();
    }

    public function next()
    {
        echo __METHOD__, PHP_EOL;
        return parent::next();
    }

    public function getInnerIterator()
    {
        echo __METHOD__, PHP_EOL;
        return parent::getInnerIterator();
    }
}
$iterator = new OutIterator(new ArrayIterator([1, 2, 3]));
foreach ($iterator as $k => $v) {
    echo $k, ': ', $v, PHP_EOL;
}
// OutIterator::rewind
// OutIterator::valid
// OutIterator::current
// OutIterator::key
// 0: 1_suffix
// OutIterator::next
// OutIterator::valid
// OutIterator::current
// OutIterator::key
// 1: 2_suffix
// OutIterator::next
// OutIterator::valid
// OutIterator::current
// OutIterator::key
// 2: 3_suffix
// OutIterator::next
// OutIterator::valid

我们自己写了一个 OutIterator 类并继承自 IteratorIterator 类,然后重写所有迭代器相关的方法函数。在这些函数中,增加一些输出调试信息,最后通过 foreach 来遍历迭代器。可以看出,foreach 在判断对象是否可迭代后,就会像我们使用 while 遍历迭代器一样地去调用对应的迭代器方法函数。这个例子相当地直观,也非常有助于我们理解迭代器这堆方法函数到底在干嘛。

var_dump($iterator->getInnerIterator());
// object(ArrayIterator)#5 (1) {
//     ["storage":"ArrayIterator":private]=>
//     array(3) {
//       [0]=>
//       int(1)
//       [1]=>
//       int(2)
//       [2]=>
//       int(3)
//     }
//   }

通过 getInerIterator() 方法我们就可以获得包装迭代器内部的那个迭代器对象。这里很清晰地就能看到我们给它内部放置的那个迭代器相关的信息。

接下来,我们再学习一些派生自 IteratorIterator 类的迭代器。也就是说,它们都是继承自 IteratorIterator 这个包装迭代器的,并在它的基础之上又增加了不少别致的功能。

AppendIterator 追加迭代器

追加迭代器,很奇怪的名字,先来看看它是做啥的。

$appendIterator = new AppendIterator();
$appendIterator->append(new ArrayIterator([1, 2, 3]));
$appendIterator->append(new ArrayIterator(['a' => 'a1', 'b' => 'b1', 'c' => 'c1']));
var_dump($appendIterator->getIteratorIndex()); // int(0)
foreach ($appendIterator as $k => $v) {
    echo $k, ': ', $v, PHP_EOL;
    echo 'iterator index: ', $appendIterator->getIteratorIndex(), PHP_EOL;
}
// 0: 1
// iterator index: 0
// 1: 2
// iterator index: 0
// 2: 3
// iterator index: 0
// a: a1
// iterator index: 1
// b: b1
// iterator index: 1
// c: c1
// iterator index: 1

var_dump($appendIterator->getIteratorIndex()); // NULL

是的,你没看错,这个追加迭代器的功能就是可以在它里面保存多个内部迭代器。我们可以通过 append() 方法不断地添加,通过 getIteratorIndex() 可以查看到当前使用或遍历到的是哪个一个内部迭代器。

如果要获取内部迭代器对象的话,虽然也有继承自 IteratorIterator 的 getInnerIterator() 方法,但最好使用另一个方法。

var_dump($appendIterator->getArrayIterator());
// object(ArrayIterator)#2 (1) {
//     ["storage":"ArrayIterator":private]=>
//     array(2) {
//       [0]=>
//       object(ArrayIterator)#7 (1) {
//         ["storage":"ArrayIterator":private]=>
//         array(3) {
//           [0]=>
//           int(1)
//           [1]=>
//           int(2)
//           [2]=>
//           int(3)
//         }
//       }
//       [1]=>
//       object(ArrayIterator)#9 (1) {
//         ["storage":"ArrayIterator":private]=>
//         array(3) {
//           ["a"]=>
//           string(2) "a1"
//           ["b"]=>
//           string(2) "b1"
//           ["c"]=>
//           string(2) "c1"
//         }
//       }
//     }
//   }

getArrayIterator() 可以一个数组形式的集合来返回所有的内部迭代器。

CachingIterator 缓存迭代器

从英文名字就可以看出来,缓存迭代器。

$cachingIterator = new CachingIterator(new ArrayIterator([1, 2, 3]), CachingIterator::FULL_CACHE);
var_dump($cachingIterator->getCache());
// array(0) {
// }
foreach ($cachingIterator as $c) {

}
var_dump($cachingIterator->getCache());
// array(3) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//   }

它比较有特色的就是这个 getCache() 方法,从上面的测试代码中大家看出什么问题了吗?没错,当我们遍历一次迭代器之后,内部迭代器的数据信息会缓存到 getCache() 这个方法里面返回的数组中。我们在遍历之前调用 getCache() 方法是没有任何内容的。另外,通过构造参数的第二个参数,我们可以指定缓存数据的信息内容,在这里我们使用的是 CachingIterator::FULL_CACHE ,也就是缓存全部内容。

FilterIterator 过滤迭代器

过滤这个词熟悉不,array_filter() 这个函数也是针对数组进行过滤操作的。同样地,FilterIterator 迭代器也是实现类似的效果。不过在学习使用这个 FilterIterator 之前,我们先学习一下它的两个派生类。

$callbackFilterIterator = new CallbackFilterIterator(new ArrayIterator([1, 2, 3, 4]), function ($current, $key, $iterator) {
    echo $key, ': ', $current, PHP_EOL;
    if ($key == 0) {
        var_dump($iterator);
    }
    if ($current % 2 == 0) {
        return true;
    }
    return false;
});
foreach ($callbackFilterIterator as $c) {
    echo 'foreach: ', $c, PHP_EOL;
}
// 0: 1
// object(ArrayIterator)#13 (1) {
//   ["storage":"ArrayIterator":private]=>
//   array(4) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//     [3]=>
//     int(4)
//   }
// }
// 1: 2
// foreach: 2
// 2: 3
// 3: 4
// foreach: 4

CallbackFilterIterator 迭代器是通过我们在构造参数的第二个参数指定的回调函数来进行过滤操作的一个迭代器。如果要让数据通过,返回 true ,否则就返回 false 。先讲这个迭代器正是因为它和 array_filter() 实在是太像了。array_filter() 也是一样的通过一个回调函数来进行过滤判断的。

$regexIterator = new RegexIterator(new ArrayIterator(['test1', 'test2', 'opp1', 'test3']), '/^(test)(\d+)/', RegexIterator::MATCH);

var_dump(iterator_to_array($regexIterator));
// array(3) {
//     [0]=>
//     string(5) "test1"
//     [1]=>
//     string(5) "test2"
//     [3]=>
//     string(5) "test3"
//   }

$regexIterator = new RegexIterator(new ArrayIterator(['test1', 'test2', 'opp1', 'test3']), '/^(test)(\d+)/', RegexIterator::REPLACE);
$regexIterator->replacement = 'new $2$1'; 
var_dump(iterator_to_array($regexIterator));
// array(3) {
//     [0]=>
//     string(9) "new 1test"
//     [1]=>
//     string(9) "new 2test"
//     [3]=>
//     string(9) "new 3test"
//   }

RegexIterator 相信也不用多解释了,它就是通过正则表达式来进行过滤判断的。在这里需要注意的是,我们使用了一个 iterator_to_array() 函数,它也是 SPL 中的一个函数,作用就是将迭代器转换为数组,其实也就是解决我们都要写 foreach 或者 while 循环来演示的麻烦。

通过上面两个 FilterIterator 的派生类的学习,相信大家对于这个过滤迭代器更加有兴趣了。不过,这个原始的 FilterIterator 是一个抽象类哦,也就是说,它是不能直接实例化的,我们只能去再写一个类来继承它,并且要实现它的一个核心方法 accept() 。

class MyFilterIterator extends FilterIterator{
    public function accept(){
        echo  __METHOD__, PHP_EOL;
        if($this->current()%2==0){
            return true;
        }
        return false;
    }
}
$myFilterIterator = new MyFilterIterator(new ArrayIterator([1,2,3,4]));
var_dump(iterator_to_array($myFilterIterator));
// MyFilterIterator::accept
// MyFilterIterator::accept
// MyFilterIterator::accept
// MyFilterIterator::accept
// array(2) {
//   [1]=>
//   int(2)
//   [3]=>
//   int(4)
// }

不少小伙伴一定已经明白了,不管是上面的 CallbackFilterIterator 还是 RegexIterator ,都是实现了 FilterIterator 的一个实现类,它们都重写了 accept() 方法。它们通过构造函数的来传递需要的数据,在核心使用的过程中 CallbackFilterIterator 就是在 accept() 中调用了那个传递进来的回调方法,而 RegexIterator 则是在 accept() 中对内部迭代器的数据进行了正则表达式的判断。

InfiniteIterator 无限迭代器

无限迭代器?什么鬼,貌似很高大上。这是一个坑,要小心哦。

$infinateIterator = new InfiniteIterator(new ArrayIterator([1,2,3,4]));
$i = 20;
foreach($infinateIterator as $k=>$v){
    echo $k, ': ', $v, PHP_EOL;
    $i--;
    if($i <= 0){
        break;
    }
}
// 0: 1
// 1: 2
// 2: 3
// 3: 4
// 0: 1
// 1: 2
// 2: 3
// ………………
// ………………

说白了,类似实现了让 next() 到最后一个数据的时候就将指针指回第一条数据的功能。有点像循环队列的感觉,也就是说,如果我们没有限制条件的话,遍历这种无限迭代器,它将变成死循环一直不停地循环下去。

LimitIterator 数量限制迭代器

看名字就知道了,就像我们经常操作 MySQL 数据库做的翻页功能一样,LimitIterator 也是根据起始和偏移区间值返回一部分数据的。

$limitIterator = new LimitIterator(new ArrayIterator([1,2,3,4]),0,2);
var_dump(iterator_to_array($limitIterator));
// array(2) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//   }

$limitIterator = new LimitIterator(new ArrayIterator([1,2,3,4]),1,3);
var_dump(iterator_to_array($limitIterator));
// array(3) {
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//     [3]=>
//     int(4)
//   }

NoRewindIterator 无重回迭代器

最后一个要介绍的 IteratorIterator 系列中的迭代器就是这个 NoRewindIterator 。同样地从名字中我们可以看出一些端倪,那就是这个迭代器是没有 rewind() 方法的,或者说,这个方法是不起作用的。

$noRewindIterator = new NoRewindIterator(new ArrayIterator([1,2,3,4]));
var_dump(iterator_to_array($noRewindIterator));
// array(4) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//     [3]=>
//     int(4)
//   }
$noRewindIterator->rewind();
var_dump(iterator_to_array($noRewindIterator));
// array(0) {
// }

前面我们看到过,在 foreach() 时,每次遍历开始时都会调用 rewind() 方法让数据指针回到最顶部。同样地,iterator_to_array() 方法在其内部实现也是这样类似的步骤。但如果是 NoRewindIterator 的话,第二次遍历就不会有内容了,因为它的 rewind() 方法是不生效的,或者说是一个空的方法。

大家可以算大尝试用 while() 循环那种方式来测试一下,比使用 iterator_to_array() 更加清晰一些。

MultipleIterator 多并行迭代器

走出了 IteratorIterator 之后,我们来看一个和它没什么关系的迭代器,也就是说,这个迭代器没有继承或者使用 IteratorIterator 相关的方法函数内容。

从名字来说,Multiple 是多个的意思,难道是内部放了多个迭代器?这不是和 AppendIterator 一样了。好吧,我承认,它确实在内部保存了一些迭代器,但注意,这些不是内置迭代器,和 IteratorIterator 是不同的哦。另外,它的表现形式也和 AppendIterator 不同。

$multipleIterator = new MultipleIterator();
$multipleIterator->attachIterator(new ArrayIterator([1,2,3,4]));
$multipleIterator->attachIterator(new ArrayIterator(['a' => 'a1', 'b' => 'b1', 'c' => 'c1']));
$arr1 = new ArrayIterator(['a', 'b', 'c']);
$arr2 = new ArrayIterator(['d', 'e', 'f', 'g', 'h']);
$multipleIterator->attachIterator($arr1);
$multipleIterator->attachIterator($arr2);

var_dump($multipleIterator->containsIterator($arr1)); // bool(true)
$multipleIterator->detachIterator($arr1);
var_dump($multipleIterator->containsIterator($arr1)); // bool(false)

// iterator_to_array($multipleIterator);
foreach($multipleIterator as $k=>$v){
    var_dump($k);
    var_dump($v);
}
// array(3) {
//     [0]=>
//     int(0)
//     [1]=>
//     string(1) "a"
//     [2]=>
//     int(0)
//   }
//   array(3) {
//     [0]=>
//     int(1)
//     [1]=>
//     string(2) "a1"
//     [2]=>
//     string(1) "a"
//   }
//   array(3) {
//     [0]=>
//     int(1)
//     [1]=>
//     string(1) "b"
//     [2]=>
//     int(1)
//   }
//   array(3) {
//     [0]=>
//     int(2)
//     [1]=>
//     string(2) "b1"
//     [2]=>
//     string(1) "b"
//   }
//   array(3) {
//     [0]=>
//     int(2)
//     [1]=>
//     string(1) "c"
//     [2]=>
//     int(2)
//   }
//   array(3) {
//     [0]=>
//     int(3)
//     [1]=>
//     string(2) "c1"
//     [2]=>
//     string(1) "e"
//   }

我们可以通过 attachIterator() 添加迭代器,通过 containsIterator() 判断指定的迭代器是否存在,也可以通过 detachIterator() 删除某个迭代器。不过最主要的特点还是在遍历的结果。

不管是 key() 还是 current() ,返回的数据都是一个数组。其实这个数组就是每个迭代器对应的内容,比如第一个 key() 返回的是第一个迭代器的下标 0 的位置,第二个迭代器下标 a 和第三个迭代器下标 0 的位置。也就是说,它一次返回了所有迭代器第一个位置的下标信息。同理,current() 返回的也是当前这个位置的所有数据信息。

另外,我们可以看到,不同的迭代器的内部数据数量是不同的,MultipleIterator 只会以最少的那条数据的数量进行返回,这个大家可以自己尝试下哦。

自己实现一个迭代器类

讲了那么多迭代器,我们要不要自己也来简单地实现一个可以让 count() 生效的,并且有递归实现功能的,可以设置游标的迭代器。

class NewIterator implements Countable, RecursiveIterator, SeekableIterator {
    private $array = [];

    public function __construct($arr = []){
        $this->array = $arr;
    }

    // Countable
    public function count(){
        return count($this->array);
    }

    // RecursiveIterator
    public function hasChildren(){
        if(is_array($this->current())){
            return true;
        }
        return false;
    }

    // RecursiveIterator
    public function getChildren(){
        
        if(is_array($this->current())){
            return new ArrayIterator($this->current());
        }
        return null;
    }

    // Seekable
    public function seek($position) {
        if (!isset($this->array[$position])) {
            throw new OutOfBoundsException("invalid seek position ($position)");
        }

        $this->position = $position;
    }
      
      public function rewind() {
          $this->position = 0;
      }
  
      public function current() {
          return $this->array[$this->position];
      }
  
      public function key() {
          return $this->position;
      }
  
      public function next() {
          ++$this->position;
      }
  
      public function valid() {
          return isset($this->array[$this->position]);
      }
}

$newIterator = new NewIterator([1,2,3,4, [5,6,7]]);
var_dump(iterator_to_array($newIterator));
// array(5) {
//     [0]=>
//     int(1)
//     [1]=>
//     int(2)
//     [2]=>
//     int(3)
//     [3]=>
//     int(4)
//     [4]=>
//     array(3) {
//       [0]=>
//       int(5)
//       [1]=>
//       int(6)
//       [2]=>
//       int(7)
//     }
//   }

var_dump(count($newIterator));
// int(5)

$newIterator->rewind();
while($newIterator->valid()){
    if($newIterator->hasChildren()){
        var_dump($newIterator->getChildren());
    }
    $newIterator->next();
}
// object(ArrayIterator)#37 (1) {
//     ["storage":"ArrayIterator":private]=>
//     array(3) {
//       [0]=>
//       int(5)
//       [1]=>
//       int(6)
//       [2]=>
//       int(7)
//     }
//   }

$newIterator->seek(2);
while($newIterator->valid()){
    var_dump($newIterator->current());
    $newIterator->next();
}
// int(3)
// int(4)
// array(3) {
//   [0]=>
//   int(5)
//   [1]=>
//   int(6)
//   [2]=>
//   int(7)
// }

关于代码不多解释了,注释里也有说明,最主要的就是要实现 Countable, RecursiveIterator, SeekableIterator 这三个接口。它们分别对应的就是 count 能力、递归能力、设置游标的能力。

总结

东西不少吧,各种迭代器的实现可以说是 SPL 中的一个非常重要的内容。除了今天介绍的这些之外,还有别的一些迭代器我们将在相关的文章中独立讲解。光是今天的内容估计就不好消化了,抓紧吸收吧,飙车还在继续哦!

测试代码:

https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/01/source/5.PHP的SPL扩展库(三)迭代器.php

参考文档:

https://www.php.net/manual/zh/spl.iterators.php

阅读 339
79 声望
15 粉丝
0 条评论
你知道吗?

79 声望
15 粉丝
文章目录
宣传栏