という話をずいぶん前に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を使いましょう!