連載:C# 2.0入門

第3回 新しい繰り返しのスタイル − yield return文とForEachメソッド

株式会社ピーデー 川俣 晶
2007/07/31
Page1 Page2 Page3 Page4

制約の真相:見た目と違う真実の姿

 これまで、反復子ブロックを使った際のいくつかの制約について見てきたが、それらはすべて「反復子ブロックは普通のブロックではないから」と説明した。では、具体的にどう違うのだろうか?

 その疑問に答えるのは難しくない。

 見掛け上の姿と真の姿の差は、逆コンパイラ・ツール「Reflector」を使うことで簡単に見ることができるからだ。

.NET Tools:.NET逆コンパイラとコードを難読化するDotfuscator

 実際に、リスト2をデバッグ・ビルドした実行ファイルをReflector for .NETによりC#のソース・コードとして表示させた例を以下に示す。

 まず、Rageクラス内に生成されている、元のソース・コードになかった「<GetEnumerator>d__0」というクラスのメンバ一覧(リスト10)を見てみよう。そこにはC# 1.x時代におなじみだったMoveNextメソッドやCurrentプロパティが生成されていることが分かると思う。

[CompilerGenerated]
private sealed class <GetEnumerator>d__0 : IEnumerator<int>, IEnumerator, IDisposable
{
  // Fields
  private int <>1__state;
  private int <>2__current;
  public Range <>4__this;
  public int <i>5__1;

  // Methods
  [DebuggerHidden]
  public <GetEnumerator>d__0(int <>1__state);
  private bool MoveNext();
  [DebuggerHidden]
  void IEnumerator.Reset();
  void IDisposable.Dispose();

  // Properties
  int IEnumerator<int>.Current { [DebuggerHidden] get; }
  object IEnumerator.Current { [DebuggerHidden] get; }
}
リスト10 自動生成された<GetEnumerator>d__0クラスのメンバ一覧

 このクラスの中で、MoveNextメソッドの内容だけ見てみることにしよう。

private bool MoveNext()
{
  switch (this.<>1__state)
  {
    case 0:
      this.<>1__state = -1;
      this.<i>5__1 = this.<>4__this.from;
      while (this.<i>5__1 <= this.<>4__this.to)
      {
        this.<>2__current = this.<i>5__1;
        this.<>1__state = 1;
        return true;
      Label_004F:
        this.<>1__state = -1;
        this.<i>5__1++;
      }
      break;

    case 1:
      goto Label_004F;
  }
  return false;
}
リスト11 MoveNextメソッドの内容(リスト2の逆コンパイル結果の一部)

 ちなみに、このコードは、一応C#の文法に沿って表記されているとはいえ、読みにくいので簡単に説明しておくと、変数「this.<>1__state」には、繰り返しの継続を行うか否かの情報が保存されている。この値をswitch文で判定し、0なら繰り返しの開始を行い、1なら繰り返しの途中に戻って継続するという処理を行う。そして、変数「this.<i>5__1」は繰り返しの回数をカウントするために使われる。繰り返しが終了すると、「this.<>1__state」は-1という値でwhile文を抜けることになるが、この-1はもはやswitch文で何もアクションを起こさない「繰り返しが終わった」ことを示す値となる。

 しかし、「<」や「>」のような表面的にコンパイルできない表記は別として考えても、このコードは根本的にC#の文法に反していてコンパイルできない。whileループの途中でreturn文により戻り、次に呼ばれたときにgoto文でループの途中へ戻るようなコードは、C#では許されない(このコードは実際にはC#ではなくILで記述されていて、ILレベルでは正しいコードになっている。C#で許されないコードが生成されてしまうのは、Reflector for .NETがILのコードを強制的にC#に翻訳しているため)。

 しかし、このようなコードを見ると、反復子という機能が実はシンタックス・シュガー(糖衣構文=より簡単かつ分かりやすく記述できる別の書き方を提供するための構文上のサービス)にすぎないことがよく分かるだろう。

 つまり反復子とは、短いソース・コードから本来あるべき長いソース・コードへの翻訳サービスそのものなのである。その過程で、元のソースは切り刻まれ、似ても似つかない形に変形される。反復子に課せられた制約とは、このC# 2.0コンパイラによる変換作業の都合によって課せられた制約だといえる。

 例えば、try〜catch構文でyield return文が使用できないのは、途中で実行を打ち切って、後から続きを継続実行するようなコードとうまく整合しないためだろう。しかし、try〜finally構文の方は、IDisposeインターフェイスをうまく活用し、実質的にfinallyブロック相当の機能を実現することで、利用可能としている。

 この辺りは、いろいろなコードを作成してからReflector for .NETで調べてみると、興味深いことが分かる。もっとストレートにC# 2.0言語仕様*1見てもよい。そこには、多くの機能性と制約が詳しく書かれている。

*1 Visual Studio 2005をインストールしている場合には、Visual Studio 2005のインストール・ディレクトリ配下の「VC#\Specifications\C# Language Specification 2.0.doc」にある。

ForEachメソッドを使う別解

 ……とまあここまで素晴らしいC# 2.0の反復子について見てきたわけだが、実はC# 2.0にはもう1つの画期的な繰り返し方法が提供されている。といっても、言語仕様が拡張されているわけではない。実は、コレクションのクラスで「ForEachメソッド」が利用できるようになったのである。

 アイデアは簡単で、繰り返しを行うメソッドを用意し、繰り返し1回ごとに実行するメソッドをデリゲート型経由で渡せるようにしただけである。

using System;

class Program
{
  static void Main(string[] args)
  {
    int[] array = { 1, 2, 3 };
    int sum = 0;

    Array.ForEach<int>(array, delegate(int i)
    {
      sum += i;
    });
    Console.WriteLine(sum);
    // 出力:6
  }
}
リスト12 配列に対するForEachメソッドの利用例

 ここで使用しているArrayクラス(System名前空間)のForEach<T>メソッドは、第1引数の配列のすべての要素に対して、第2引数のデリゲートを呼び出す機能を持つ。

 このようなメソッドはC# 1.xの時代にも容易に記述できたが、やはり実用性が出てきたのは匿名メソッドが使えるようになったC# 2.0以降だろう。匿名メソッドなら、上位のブロックのスコープに属するので、上記のサンプル・コードのように変数sumに値を積算することも容易に可能である*2

*2 匿名メソッドについては、詳しくは第5回で解説予定であるが、第1回でも簡単に解説しているので、ここではそれを参考にしていただきたい。

 当然のことながら、ForEachメソッドの自作も容易である。

 リスト2に相当するコードを、ForEachメソッドを自作することによって実現してみよう。

using System;

class Range
{
  public static void ForEach(int from, int to, Action<int> action)
  {
    for (int i = from; i <= to; i++) action(i);
  }
}

class Program
{
  static void Main(string[] args)
  {
    Range.ForEach(0, 9,
      delegate(int i) // 匿名メソッド
      {
        Console.Write("{0} ", i);
      }
    );
    // 出力:0 1 2 3 4 5 6 7 8 9
  }
}
リスト13 リスト2に相当するコードをForEachメソッドで記述した

 このとおり、何も難しいことはない。内容も簡潔である。

性能比較

 さて、ここで気になるのは反復子とForEachメソッドのどちらを使うべきかである。ソース・コードの簡潔さは大差ないとすれば、残ったポイントは性能といえる。

 そこで、反復子とForEachメソッドの速度比較を行ってみた。リスト14はそのために作成したテスト・プログラムである。

using System;
using System.Collections.Generic;

class Program
{
  private static IEnumerable<int> getCounter(int from, int to)
  {
    for (int i = from; i <= to; i++) yield return i;
  }

  private static void forEach(int from, int to, Action<int> action)
  {
    for (int i = from; i <= to; i++) action(i);
  }

  static void Main(string[] args)
  {
    const int TestCount = 1000000000;

    DateTime start1 = DateTime.Now;
    int sum1 = 0;

    foreach (int i in getCounter(1, TestCount))
    {
      sum1 += i;
    }
    Console.WriteLine(DateTime.Now - start1);

    DateTime start2 = DateTime.Now;
    int sum2 = 0;

    forEach(1, TestCount, delegate(int i)
    {
      sum2 += i;
    });
    Console.WriteLine(DateTime.Now - start2);

    Console.WriteLine(sum1 == sum2);
  }
}
リスト14 反復子とForEachメソッドの速度比較

00:00:13.0810000
00:00:03.2840000
True
リスト14の実行結果(Visual Studio 2005のリリース・ビルド)

 実行結果は、1行目が反復子による実行時間、2行目はForEachメソッドによる実行時間、3行目は2つの処理結果の累積値が同じ(つまり同じ回数を繰り返した証拠)としてのTrueが出力されている。

 これを見ると一目瞭然(りょうぜん)だが、ForEachメソッドを使った方が圧倒的に速い。デバッグ・ビルドにすると差は縮まるが、やはりForEachメソッドの方が速い。

 なぜForEachメソッドの方が速いのかは、内部構造を考えれば容易に分かるだろう。ForEachメソッドは、繰り返し1回ごとにメソッドを1回だけ呼ぶ。しかし、反復子はMoveNextメソッドを呼んでからCurrentプロパティで値を取得する必要があり、2回の呼び出しが発生する。さらに、周辺にかなり込み入ったコードが生成されていることから考えて、大きな性能差が出るのはやむを得ないところだろう。

 筆者はすでにかなりの量のコードをC# 2.0で書いているが、実はその間に一度もyield return文を使ったことがなかった。なぜかといえば、ForEachメソッドのテクニックを使えば、使う必要もなければ使うメリットもないからだ。しかし、配列などを列挙する場合にはforeach文を多用していた。これもあまりメリットがない。これからはForEachメソッドを積極的に使おう……と心に決めた次第である。

次回予告

 次回は「null許容型」を取り上げる。これは、整数などの値型をベースとしながらも、null値を許容する特殊な型である。

 データベース処理などで、nullを許容するフィールドから値を取得する場合、null値であってもそのまま受け取ることができて便利なものである。コレクションとの関係や、パフォーマンスに与える影響などを含め、解説していきたい。End of Article


 INDEX
  C# 2.0入門
  第3回 新しい繰り返しのスタイル − yield return文とForEachメソッド
    1.繰り返しという古くて新しい問題/数を数えるというサンプル/C# 1.xによるRangeクラスの実装
    2.C# 2.0によるRangeクラスの実装/yeild break文による中断
    3.yieldは予約語ではない/1つのクラスに複数の列挙機能を付ける/自動的に作られるオブジェクトと2重利用/catchできない制約
  4.制約の真相:見た目と違う真実の姿/ForEachメソッドを使う別解/性能比較
 
インデックス・ページヘ  「C# 2.0入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間