PHP的SPL擴展庫(三)迭代器
關于迭代器,,我們在之前設計模式相關的文章中已經(jīng)講過迭代器具體是個啥,,而且也使用過 SPL 的例子來演示過,要是沒有看過之前的文章的可以穿越回去看一下哦,!PHP設計模式之迭代器模式:https://mp.weixin.qq.com/s/uycac0OXYYjAG1BlzTUjsw,。
因此,,對于迭代器的概念,,我們這里就不會多說了,今天的主要內(nèi)容就是來了解一下 SPL 擴展中都包含哪些迭代器以及它們的功能效果,。另外,,上一篇文章中我們接觸過的數(shù)組迭代器 ArrayIterator 由于已經(jīng)學習過了,也就不放在這里講了,。此外還有文件目錄相關的迭代器,,也會放在和其相關的文件目錄操作的文章中講解,,包括下面學習的這些迭代器還有不少都有相對應的 遞歸式 的迭代器,,比如我們下面要講到的 CachingIterator ,、 FilterIterator 等等,,都有它們對應的 RecursiveCachingIterator ,、 RecursiveFilterIterator 類,,這個大家就自己去研究下吧,,帶遞歸迭代器,,也就是多了兩個方法 getChildren() 和 hasChildren() 而已,,最后我們還會實現(xiàn)一個自己的迭代器類,,其中就會講到遞歸這塊。
IteratorIterator 包裝迭代器
首先我們來看一下什么是包裝迭代器,。它本身也是個迭代器,,但是在實例化的時候,又必須給他再傳遞進來一個迭代器并保存在內(nèi)部,,是一個內(nèi)部迭代器 InnerIterator ,。對于它自身的那些迭代器接口函數(shù)來說,,其實都是轉(zhuǎn)發(fā)調(diào)用的那個內(nèi)部迭代器相關的操作函數(shù)。感覺上其實就有點像是一個裝飾器模式,,我們可以通過繼承 IteratorIterator 來實現(xiàn)對原有迭代器功能的升級,。
$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
從代碼中可以看出,它的構造參數(shù)必須還得是一個迭代器,,本身的參數(shù)簽名就是需要一個實現(xiàn)了 Traversable 接口的對象,。Traversable 接口是所有迭代器所必須要實現(xiàn)的接口。
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 類,,然后重寫所有迭代器相關的方法函數(shù),。在這些函數(shù)中,增加一些輸出調(diào)試信息,,最后通過 foreach 來遍歷迭代器,。可以看出,,foreach 在判斷對象是否可迭代后,,就會像我們使用 while 遍歷迭代器一樣地去調(diào)用對應的迭代器方法函數(shù)。這個例子相當?shù)刂庇^,,也非常有助于我們理解迭代器這堆方法函數(shù)到底在干嘛,。
var_dump($iterator->getInnerIterator());
// object(ArrayIterator)#5 (1) {
// ["storage":"ArrayIterator":private]=>
// array(3) {
// [0]=>
// int(1)
// [1]=>
// int(2)
// [2]=>
// int(3)
// }
// }
通過 getInerIterator() 方法我們就可以獲得包裝迭代器內(nèi)部的那個迭代器對象。這里很清晰地就能看到我們給它內(nèi)部放置的那個迭代器相關的信息,。
接下來,,我們再學習一些派生自 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
是的,,你沒看錯,,這個追加迭代器的功能就是可以在它里面保存多個內(nèi)部迭代器。我們可以通過 append() 方法不斷地添加,,通過 getIteratorIndex() 可以查看到當前使用或遍歷到的是哪個一個內(nèi)部迭代器,。
如果要獲取內(nèi)部迭代器對象的話,雖然也有繼承自 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() 可以一個數(shù)組形式的集合來返回所有的內(nèi)部迭代器。
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() 方法,,從上面的測試代碼中大家看出什么問題了嗎?沒錯,,當我們遍歷一次迭代器之后,,內(nèi)部迭代器的數(shù)據(jù)信息會緩存到 getCache() 這個方法里面返回的數(shù)組中。我們在遍歷之前調(diào)用 getCache() 方法是沒有任何內(nèi)容的,。另外,,通過構造參數(shù)的第二個參數(shù),我們可以指定緩存數(shù)據(jù)的信息內(nèi)容,,在這里我們使用的是 CachingIterator::FULL_CACHE ,,也就是緩存全部內(nèi)容。
FilterIterator 過濾迭代器
過濾這個詞熟悉不,,array_filter() 這個函數(shù)也是針對數(shù)組進行過濾操作的,。同樣地,F(xiàn)ilterIterator 迭代器也是實現(xiàn)類似的效果,。不過在學習使用這個 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 迭代器是通過我們在構造參數(shù)的第二個參數(shù)指定的回調(diào)函數(shù)來進行過濾操作的一個迭代器,。如果要讓數(shù)據(jù)通過,,返回 true ,否則就返回 false ,。先講這個迭代器正是因為它和 array_filter() 實在是太像了,。array_filter() 也是一樣的通過一個回調(diào)函數(shù)來進行過濾判斷的,。
$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() 函數(shù),,它也是 SPL 中的一個函數(shù),作用就是將迭代器轉(zhuǎn)換為數(shù)組,,其實也就是解決我們都要寫 foreach 或者 while 循環(huán)來演示的麻煩,。
通過上面兩個 FilterIterator 的派生類的學習,相信大家對于這個過濾迭代器更加有興趣了,。不過,,這個原始的 FilterIterator 是一個抽象類哦,也就是說,,它是不能直接實例化的,,我們只能去再寫一個類來繼承它,并且要實現(xiàn)它的一個核心方法 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)
// }
不少小伙伴一定已經(jīng)明白了,,不管是上面的 CallbackFilterIterator 還是 RegexIterator ,,都是實現(xiàn)了 FilterIterator 的一個實現(xiàn)類,它們都重寫了 accept() 方法,。它們通過構造函數(shù)的來傳遞需要的數(shù)據(jù),,在核心使用的過程中 CallbackFilterIterator 就是在 accept() 中調(diào)用了那個傳遞進來的回調(diào)方法,而 RegexIterator 則是在 accept() 中對內(nèi)部迭代器的數(shù)據(jù)進行了正則表達式的判斷,。
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
// ………………
// ………………
說白了,,類似實現(xiàn)了讓 next() 到最后一個數(shù)據(jù)的時候就將指針指回第一條數(shù)據(jù)的功能,。有點像循環(huán)隊列的感覺,也就是說,,如果我們沒有限制條件的話,,遍歷這種無限迭代器,它將變成死循環(huán)一直不停地循環(huán)下去,。
LimitIterator 數(shù)量限制迭代器
看名字就知道了,,就像我們經(jīng)常操作 MySQL 數(shù)據(jù)庫做的翻頁功能一樣,LimitIterator 也是根據(jù)起始和偏移區(qū)間值返回一部分數(shù)據(jù)的,。
$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() 時,,每次遍歷開始時都會調(diào)用 rewind() 方法讓數(shù)據(jù)指針回到最頂部。同樣地,,iterator_to_array() 方法在其內(nèi)部實現(xiàn)也是這樣類似的步驟,。但如果是 NoRewindIterator 的話,第二次遍歷就不會有內(nèi)容了,,因為它的 rewind() 方法是不生效的,,或者說是一個空的方法。
大家可以算大嘗試用 while() 循環(huán)那種方式來測試一下,,比使用 iterator_to_array() 更加清晰一些,。
MultipleIterator 多并行迭代器
走出了 IteratorIterator 之后,我們來看一個和它沒什么關系的迭代器,也就是說,,這個迭代器沒有繼承或者使用 IteratorIterator 相關的方法函數(shù)內(nèi)容,。
從名字來說,Multiple 是多個的意思,,難道是內(nèi)部放了多個迭代器,?這不是和 AppendIterator 一樣了。好吧,,我承認,,它確實在內(nèi)部保存了一些迭代器,但注意,,這些不是內(nèi)置迭代器,,和 IteratorIterator 是不同的哦。另外,,它的表現(xiàn)形式也和 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() ,,返回的數(shù)據(jù)都是一個數(shù)組,。其實這個數(shù)組就是每個迭代器對應的內(nèi)容,比如第一個 key() 返回的是第一個迭代器的下標 0 的位置,,第二個迭代器下標 a 和第三個迭代器下標 0 的位置,。也就是說,它一次返回了所有迭代器第一個位置的下標信息,。同理,,current() 返回的也是當前這個位置的所有數(shù)據(jù)信息。
另外,,我們可以看到,,不同的迭代器的內(nèi)部數(shù)據(jù)數(shù)量是不同的,MultipleIterator 只會以最少的那條數(shù)據(jù)的數(shù)量進行返回,,這個大家可以自己嘗試下哦。
自己實現(xiàn)一個迭代器類
講了那么多迭代器,,我們要不要自己也來簡單地實現(xiàn)一個可以讓 count() 生效的,,并且有遞歸實現(xiàn)功能的,可以設置游標的迭代器,。
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)
// }
關于代碼不多解釋了,,注釋里也有說明,最主要的就是要實現(xiàn) Countable, RecursiveIterator, SeekableIterator 這三個接口。它們分別對應的就是 count 能力,、遞歸能力,、設置游標的能力。
總結
東西不少吧,,各種迭代器的實現(xiàn)可以說是 SPL 中的一個非常重要的內(nèi)容,。除了今天介紹的這些之外,還有別的一些迭代器我們將在相關的文章中獨立講解,。光是今天的內(nèi)容估計就不好消化了,,抓緊吸收吧,飆車還在繼續(xù)哦,!
測試代碼:
https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/01/source/5.PHP的SPL擴展庫(三)迭代器.php
參考文檔:
https://www./manual/zh/spl.iterators.php