PHP的SPL擴充套件庫(三)迭代器

硬核專案經理發表於2021-10-19

關於迭代器,我們在之前設計模式相關的文章中已經講過迭代器具體是個啥,而且也使用過 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

相關文章