2013/11/25

C#Tips6 反復子を使おう

Tips1でforeachで反復できる実装の仕方を紹介しました。
要は、foreach(★ in ●)に対して、●を返すためにSystem.Collections.IEnumerableインターフェースを実装して、GetEnumerator()によって取得できるようにしました。また、★にデータを入れるためにSystem.Collections.IEnumeratorインターフェースを実装しました。

ところでC#にも最近流行りの反復子と呼ばれる機構が備わっています。
次の例では
10
20
30
と表示されます。
反復子を実装するメソッドには戻り値としてSystem.Collections.IEnumerableインターフェース型を返します。
反復子はyield returnで、yield単体では意味を持ちません。従ってyieldという変数名は使用可能です。(しない方がいいです)
yield returnがあらわれると、値を返して、メソッド内の処理を一時中断します。
そして再びforeachによって内部でGetEnumerator()が呼ばれると前の処理から再開して次のyield returnまで実行されます。



class Program {
    public static void Main() {
        foreach(int x in getValues()) {
            System.Console.WriteLine(x);
        }
    }

    static System.Collections.IEnumerable getValues() {
        yield return 10;
        yield return 20;
        yield return 30;
    }
}

ところが、例えば次のようにしてもコンパイルエラーになりません。

class Program {
    public static void Main() {
        foreach(string x in getValues()) {
            System.Console.WriteLine(x);
        }
    }

    static System.Collections.IEnumerable getValues() {
        yield return 10;
        yield return 20;
        yield return 30;
    }
}

string型で受け取れるはずがないので、例外が発生します。
コンパイル時に型チェックを有効にするために、System.Collections.Generic.IEnumerable<int>を実装します。
こうすることで次の例ではしっかりエラーになります。

class Program {
    public static void Main() {
        foreach(string x in getValues()) {
            System.Console.WriteLine(x);
        }
    }

    static System.Collections.Generic.IEnumerable<int> getValues() {
        yield return 10;
        yield return 20;
        yield return 30;
    }
}

yield returnはもちろんfor文などの中で使用可能です。むしろそのためにあります。
クラスを使う人のために、極力クラスをfor文で回したりしないように実装します。
例えば本クラスで、本の全てを列挙するためにlistup()を実装しました。
更に逆順に列挙するためにlistupReverse()を実装しました。
利用者は、内部のデータ構造を知らなくても昇順・逆順で列挙することができるのです。

struct Book {
    public string title;
}

class Booklist {
    Book[] books;

    public Booklist() {
        books = new Book[10];

        for(int i = 0; i < 10; i++) {
            books[i].title = ((char)('a' + i)).ToString();
        }
    }

    public System.Collections.Generic.IEnumerable<Book> listup() {
        for(int i = 0; i < 10; i++) {
            yield return books[i];
        }
    }
   
    public System.Collections.Generic.IEnumerable<Book> listupReverse() {
        for(int i = 9; i >= 0; i--) {
            yield return books[i];
        }
    }
}


class Program {
    public static void Main() {
        Booklist bl = new Booklist();

        foreach(Book book in bl.listup()) {
            System.Console.WriteLine(book.title);
        }
       
        foreach(Book book in bl.listupReverse()) {
            System.Console.WriteLine(book.title);
        }
    }
}

結局、yieldを使うと前に紹介したIEnumeratorを実装したクラスが生成され、反復されるだけです。

というわけで要素数100000000でベンチマークをしてみました。

前回のIEnumerator実装 00:00:02.5961485
今回のyield return実装 00:00:02.3300033

余り変わらないですね。今回のソースコードは以下の通りです。

class MyClass {
    int[] ary;
    const int MAX_SIZE = 100000000;

    public MyClass() {
        ary = new int[MAX_SIZE];
    }

    public void setSeriesData() {
        for(int i = 0; i < MAX_SIZE; i++) {
            ary[i] = i;
        }
    }

    public System.Collections.Generic.IEnumerable<int> GetEnumerator() {
        for(int i = 0; i < MAX_SIZE; i++) {
            yield return ary[i];
        }
    }
}

class Program {
    public static void Main() {
        MyClass mc = new MyClass();
        long sum = 0;
        mc.setSeriesData();
        System.DateTime dt = System.DateTime.Now;
        foreach(int x in mc.GetEnumerator()) {
            sum += x; // 反復
        }
        System.Console.WriteLine(System.DateTime.Now - dt);
    }
}

0 件のコメント:

コメントを投稿