PHP

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を使いましょう!