[C#] Iterator パターン – デザインパターン入門

[C#] Iterator パターン – デザインパターン入門

デザインパターン入門

『Java言語で学ぶ デザインパターン入門』という本に書かれているデザインパターンのサンプルを C# で書き換えて勉強していこうと思います。

今回は Iterator パターンです。

Iterator パターン

GoF によって定義されたデザインパターンの1つです。内部のデータ構造に依存せず、反復処理(ループ)を抽象化するためのデザインパターンです。

C# の場合、簡単に言うとforeach でループできるようにするのが Iterator パターンです。

ちなみに Iterator は 反復子と訳されます。

抽象的なループ

例えば配列をループする例を考えます。よくあるのは for 文を使ってインデックスを加算しつつ全要素分繰り返す方法です。

var a = new int[] { 1, 2, 3 };
for (int i = 0; i < a.Length; i++)
{
    var x = a[i];
    // ..
}

上の例のfor文だと現在のインデックスを表す変数iを増加させながら配列の要素を参照しています。この変数iの働きを抽象化して一般化したのが Iterator パターンです。

for文でくるくる回すよりも foreach で回すほうが抽象的に走査できます。

var a = new int[] { 1, 2, 3 };
foreach (var x in a)
{
    // ..
}

for 文より foreach のほうが a の型に依存せずに処理できます。aが配列でもリストでも独自の型でも、いい感じに走査できます。

サンプル

例えばBook(本)クラスとBookShelf(本棚)クラスを作ります。本棚には本が入っていて、これを全件ループするようや処理を考えます。

class Book
{
    public string Title { get; set; }

    public Book(string title)
    {
        this.Title = title;
    }
}
class BookShelf
{
    public List<Book> Books { get; } = new List<Book>();

    public void Add(Book book)
    {
        this.Books.Add(book);
    }
}

for文だと以下のような感じです。

var bookShelf = new BookShelf();
bookShelf.Add(new Book("AAA"));
bookShelf.Add(new Book("BBB"));
bookShelf.Add(new Book("CCC"));

for (int i = 0; i < bookShelf.Books.Count; i++)
{
    // ..
}

この Book のループをインデックスを使わずに実装するために Iterator パターンを使います。

Iterator パターンの実装

まずはインターフェースを作成します。

interface IAggregate<T>
{
    IIterator<T> Iterator();
}

interface IIterator<T>
{
    bool HasNext();
    T Next();
}

IAggregate<T> がコレクションの役割を果たすクラスに実装させるインターフェースです。イテレーターを取得できるようにします。

class BookShelf : IAggregate<Book>
{
    public List<Book> Books { get; } = new List<Book>();
    public void Add(Book book)
    {
        this.Books.Add(book);
    }

    public IIterator<Book> Iterator()
    {
        return new BookShelfIterator(this);
    }
}

こんな感じです。イテレーターを生成して返すようにしています。

IIterator<T> はループに関する状態を管理するためのクラスに実装します。

class BookShelfIterator : IIterator<Book>
{
    public BookShelf BookShelf { get; }
    public int Index { get; private set; } = 0;

    public BookShelfIterator(BookShelf bookShelf)
    {
        this.BookShelf = bookShelf;
    }

    public bool HasNext()
    {
        return Index < this.BookShelf.Books.Count;
    }

    public Book Next()
    {
        var book = this.BookShelf.Books[this.Index++];
        return book;
    }
}

ここが Iterator パターンの肝で、なにをやっているかというと、ループする際の状態(インデックス)を管理しています。つまりfor文で管理していたインデックスをこのクラス内部に閉じ込めています。

めんどくさいので BookShelf クラスの中のリストを直接参照していますが、ようはインデックスに対応した要素と要素の数を取得できれば同じように実装できます。現在のインデックスが要素数を超えていれば HasNext() が false になります。Next() は現在の要素を返してインデックスを進めます。

このように実装することでfor文を次のように抽象化できます。

var bookShelf = new BookShelf();
bookShelf.Add(new Book("AAA"));
bookShelf.Add(new Book("BBB"));
bookShelf.Add(new Book("CCC"));

var iterator = bookShelf.Iterator();
while (iterator.HasNext())
{
    var book = iterator.Next();
    Console.WriteLine(book.Title);
}

冗長な記述に感じますが、私も冗長に感じます。しかし有用なパターンなので C# だと言語レベルですでに用意されています。それが IEnumerable インターフェースです。実装してみます。

IEnumerable

class BookShelf: IEnumerable<Book>
{
    public List<Book> Books { get; } = new List<Book>();

    public void Add(Book book)
    {
        this.Books.Add(book);
    }

    public IEnumerator<Book> GetEnumerator()
    {
        for (int i = 0; i < this.Books.Count; i++)
        {
            yield return this.Books[i];
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

IEnumerable は GetEnumerator() の実装を要請します。ここで列挙したい値を yield return で返します。

ここでは Book 型のデータをループしたいので型引数で指定して実装しています。これで foreach でループできるようになります。IEnumerable.GetEnumerator はジェネリックがない時代の名残です。GetEnumerator() を使いまわしましょう。

var bookShelf = new BookShelf();
bookShelf.Add(new Book("AAA"));
bookShelf.Add(new Book("BBB"));
bookShelf.Add(new Book("CCC"));

foreach (var book in bookShelf)
{
    Console.WriteLine(book.Title);
}

こんな感じで直接 BookShelf 型を foreach でループして Book を取得できるようになりました。

書籍の実装との比較

書籍の実装例では以下のようなループになります。

while (it.hasNext()) {
  Book book = (Book)it.next();
  // ..
}

C# で IEnumerable を実装したクラスはこのようなループも書くことができます。

var enumerator = bookShelf.GetEnumerator();
while (enumerator.MoveNext())
{
    var book = enumerator.Current;
    // ..
}

こんな感じでも使えるので Iterator パターンが実装できていることになります。というよりこの処理を簡潔にしたのが foreach 文となります。

実際配列やリストなどが foreach できるのは IEnumerable を実装しているためです。

Iterator パターンのメリット

Iterator パターンのメリットはデータの内部構造によらずにループでの走査を実装できることにあります。例えば上で書いた BookShelf クラスは内部で List を使ってデータを管理していましたが、仮に Dictionary でデータを管理するように変更された場合も GetEnumerator() の実装を変更すればいい感じに動かせます。

class BookShelf: IEnumerable<Book>
{
    public Dictionary<string, Book> Books { get; } = new Dictionary<string, Book>();

    public void Add(Book book)
    {
        this.Books.Add(book.Title, book);
    }

    public IEnumerator<Book> GetEnumerator()
    {
        foreach (var pair in this.Books)
        {
            yield return pair.Value;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

これがメリットです。内部のデータ構造によらずループを抽象的に描くことができるようになりました。

C# だと自前で Iterator のインターフェースを作成する意味はほとんどありません。Iterator パターンを実装する場合には IEnumerable インターフェースを使いましょう。

参考URL

CI/CDカテゴリの最新記事