タイトルで結論を言い切った感もありますが、さておき。
Iterator / IteratorAggregate
といった interface を実装することで、PHP ではそのインスタンスを foreach で回せるようになります。が、これには一点見落としがちな挙動があります。
以下のようなクラスを用意して実例を見てみましょう。
<?
class HogeList implements Iterator {
private $position = 0;
private $array;
public function __construct($array){
$this->array = $array;
$this->position = 0;
}
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 array_key_exists($this->position, $this->array);
}
}
簡単なテストでは何も問題なさそうに見えますが…
<?
$list = new HogeList([1, 2]);
foreach($list as $i){
echo $i;
}
// 1 2
以下のように、同一のイテレータを利用して foreach を入れ子にすると挙動が怪しくなります。
<?
$list = new HogeList([1, 2]);
foreach($list as $i){// A
foreach($list as $i)// B
echo $i;
}
// 1 2
要素 2 個の配列を回しているのだから "1 2 1 2" が出力されるかと思いきや、"1 2" しか出力されていません。このように動くのは、B を抜けたときにすでに $list 内のカーソルは最後を指していて、そのまま A のループまで抜けてしまうためです。仕様と言えば仕様ですが、しかしこの HogeList の使い勝手はあまり良さそうではありません。
参考として array をそのままループさせた場合も試してみます。
<?
$list = [1, 2];
foreach($list as $i){
foreach($list as $i)
echo $i;
}
// 1 2 1 2
やはりこの挙動のほうがクラスを利用する側としては安心できますね。
ということで HogeList は IteratorAggregate を実装する形に変更してしまいましょう。
<?
class HogeList implements IteratorAggregate{
private $array;
public function __construct($array){
$this->array = $array;
}
public function getIterator(){
return new HogeIterator($this->array);
}
}
この形であれば foreach を実行するたびに getIterator が呼ばれるので、先のような挙動の心配はありません。よくあるボイラープレートが隠れるためにコードの見通しがよくなるという意味でも、こちらの実装に分がありそうです。( HogeIterator は上記 iterator.php の HogeList と同じなのでコードは省略 )
実際に動かしてみると
<?
$list = new HogeList([1, 2]);
foreach($list as $i){
foreach($list as $i)
echo $i;
}
// 1 2 1 2
期待する出力が得られることを確認できます。めでたし。
まとめ
- IteratorAggregate と Iterator は "単なる foreach で回すための interface であってどちらも動作に大差ない" という認識は誤り。
- ライブラリとしてクラスを公開する際には、Iterator は委譲のために利用しておき、表には IteratorAggregate として提供するといった設計のほうが無難。
- ここで毎回同じ Iterator を返したりすると元の木阿弥となるのでご注意を。