LoginSignup
136

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-06-12

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
136