Edited at

Iteratorをimplementsする奴は情弱。IteratorAggregateを使え

More than 3 years have passed since last update.

という話をずいぶん前にFabienさんがしていたのをふと思い出したので、たまには啓蒙っぽい記事を書いてみる。

Iterator or IteratorAggregate? - Fabien Potencier


foreach可能なオブジェクトを作る

PHPのオブジェクトは特に何もしなくてもforeachでぐるぐる回して中身を得ることができます。

ただし、得られるものはpublicメンバに限ります。

<?php

class A {
public $hoge = 1;
public $fuga = 2;
private $pri = 3;
protected $pro = 4;
}

$a = new A;

foreach ($a as $key => $val) {
var_dump($key, $val);
}

string(4) "hoge"

int(1)
string(4) "fuga"
int(2)

ただ、カプセル化を真面目に行っている皆様方におかれましては、publicなんて使うことはないでしょう。(!)

privateやprotectedなメンバを含むクラスが通常で、そういった複雑なクラスをforeachでうまいこと回せるようにしたいなら、工夫が必要になります。


Iteratorって面倒くさいよねって話

IteratorというPHPの組み込みインターフェースを実装しておくと、オブジェクトをforeachループに突っ込んだ際の挙動を細かく制御できるようになります。

しかし、このIteratorは5つもメソッドを実装せねばならず、面倒くさいです。

interface Iterator extends Traversable

{
function current();
function key();
function next();
function rewind();
function valid();
}

//冒頭のFabienさんの記事から丸ごと引用

class Foo implements Iterator
{
protected $attributes = array();

function __construct(array $arr)
{
$this->attributes = $arr;
}

function rewind()
{
reset($this->attributes);
}

function current()
{
return current($this->attributes);
}

function key()
{
return key($this->attributes);
}

function next()
{
return next($this->attributes);
}

function valid()
{
return false !== current($this->attributes);
}
}

うえー。。だいたい決まりきった書き方とはいえ、これ全部にユニットテスト書くの? 書きたくねーだろ!


IteratorAggregateならこんなに簡単

IteratorAggregateもPHPの組み込みインターフェースであり、実装するとforeachで回せるようになります。

そういう意味ではIteratorと似ているんですが、こちらはIteratorオブジェクトを作るところだけ行い、クラス自身では反復の管理を行いません。

なので実装するべきはgetIteratorメソッドただ一つでOK。getIteratorメソッドの中で、新しくIteratorクラスをnewして、返せばいいだけです。

interface IteratorAggregate extends Traversable

{
function getIterator();
}

class Foo implements IteratorAggregate

{
protected $attributes = array();

function __construct(array $arr)
{
$this->attributes = $arr;
}

function getIterator()
{
return new ArrayIterator($this->attributes);
}
}

これだけでFooはforeachで回してattributesの中身を取り出せるようになります。

ちなみにIteratorAggregateはPHP5.5から導入されたジェネレータと大変相性がいいです。getIteratorメソッドそのものをジェネレータ関数にしてもよいのです。

class Foo implements IteratorAggregate

{
protected $attributes = array();

function __construct(array $arr)
{
$this->attributes = $arr;
}

function getIterator()
{
foreach ($this->attributes as $key => $val) {
yield $key => $val;
}
}
}

複雑なループを行いたい場合はジェネレータの方が書きやすいでしょう。

SPLには結構いろんなIteratorが用意されているので、こいつらを活用すれば自前でIteratorを実装しなければならないケースはだいぶ減ると思います。


入れ子foreachへの対応

ちなみに、Iteratorはそれ自体が反復の状態管理を行いますので、同時に複数のループを扱うことができません。具体的に言えば入れ子にすることができません。

IteratorAggregateの場合は、foreachに入るたびに新しいIteratorを返すので、入れ子に対応できます。

この点からも、IteratorAggregateを使ったほうがいいです。

参考: イテレータを介して見るPHPクラスの内部構造 - hnwの日記


速度的な話

自前実装したIteratorは、反復のたびにメソッドを何度も呼ぶはめになるため、遅いです。

ArrayIteratorなど、組み込みのIteratorを流用した方が一般的には高速に動作します。


まとめ

別に、XMLReaderとかストリームといった、低レベルの概念をIterator化するのは問題ないのですが、Webアプリとかで登場するような、ドメインレベルのクラスにIteratorをちまちま実装する行為は普通必要ないと思います。

というわけでIteratorAggregateを使いましょう!