特集

C# 2.0新機能徹底解説(後編)

進化したC# 2.0の状態管理、匿名メソッドとイテレータ

菊池 和彦
Microsoft Most Valuable Professional 2004 - Visual C#)
2004/12/28
Page1 Page2 Page3 Page4

■イテレータを解剖する

 さて、イテレータがどのようにしてこのような挙動を実現しているのか。再び逆アセンブラILDASMを使って確認してみよう。

 先ほどのサンプル・プログラムをILDASMで逆アセンブルしてみると、次の画面のようになる。

サンプル・プログラムをILDASMで逆アセンブルした画面

 上の画面を見て、「またネストしたクラスか!」と思った人も多いだろう。実はそのとおりなのだ。イテレータを実装すると、ネストしたクラスが作られるのである。

 イテレータはIEnumeratorオブジェクトを返すので、当然、それを実装するためのクラスが必要となる。implementsの部分を見ると、このクラスが確かにIEnumeratorインターフェイスとIDisposableインターフェイスを実装しているのが分かる。

 実際、上記のコードのMainメソッドは、foreach文などのシンタックス・シュガー(=薬に糖衣をかけて飲みやすくするように、文法に便宜的な構文を用意して人間が理解しやすいように・使いやすいようにすること)を使わずに以下のように記述することもできる(そもそもコンパイラはforeach文に対してこのようなコードを生成している)。

public static void Main( string[] args )
{
  OneTwoThree iter = new OneTwoThree();
  IEnumerator e = iter.GetEnumerator()
  try {
    while( e.MoveNext() ) {
      int i=e.Current;
      Console.WriteLine( "Main: value={0} ThreadID={1}",
        i,Thread.CurrentThread.ManagedThreadId );
    }
  }
  finally {
    ((IDisposable)e).Dispose();
  }
}
foreach文を使わずに記述したループ処理

 上記のコードから、IEnumeratorとIDisposableの両方のインターフェイスが必要なのが見て取れるだろう。

 このMainメソッドの流れを見ていくと、OneTwoThreeオブジェクトのGetEnumeratorメソッドが呼び出され、その戻り値としてIEnumeratorオブジェクトが返される。次に、IEnumeratorオブジェクトのMoveNextメソッドを呼んだ後、IEnumeratorオブジェクトのCurrentプロパティによりコレクション・オブジェクトの現在位置の値(int型のオブジェクト)が取得される。これらはMoveNextメソッドがTrueを返す間、繰り返される。最後にIDisposableインターフェイスのDisposeメソッドが呼び出される。

 さて、もう一度ILDASMの画面に戻って確認を進めよう。ローカル変数「i」は「<i>5__1」というフィールド変数に変換されている。これは匿名メソッドと同じ手法である。

 まず、簡単なものを先に確認しておこう。生成されたネスト・クラスにおいて、IEnumeratorインターフェイスのget_Currentメソッドの実装は以下のようなMSILコードになっている。

IL_0000:  ldarg.0
IL_0001:  ldfld      object OneTwoThree/'<GetEnumerator>d__0'::'<>2__current'
IL_0006:  stloc.0
IL_0007:  br.s       IL_0009
IL_0009:  ldloc.0
IL_000a:  ret
IEnumeratorインターフェイスのget_CurrentメソッドのMSILコード

 C#で書くと、「return <>2__current;」という1文でしかない。ここの解説をすべてMSILコードで埋め尽くすと分かりづらいので、ここからはMSILコードは示さずに、C#コードに書き戻したコードで示していくようにする。だがMSILコードそのものはぜひ一度確認してみてほしい。

 では、流れに従って残りを見ていこう。MSILコードで出力されたGetEnumeratorメソッドをC#で書き直すと以下のようになる。

IEnumerator GetEnumerator()
{
  <GetEnumerator>d__0 V_0 = new <GetEnumerator>d__0( 0 );
  V_0.<>4__this = this;
  return V_0;
}
OneTwoThreeクラスのGetEnumeratorメソッドの実装コード

 「<GetEnumerator>d__0」クラスのコンストラクタのパラメータに「0」を渡している。その後、生成されたIEnumeratorオブジェクトを戻り値で返している。

 さらに、その「<GetEnumerator>d__0」クラスのコンストラクタの内容は次のようになっている。

public <GetEnumerator>d__0(int <>1__state)
{
  this.<>1__state = <>1__state;
}
「<GetEnumerator>d__0」クラスのコンストラクタの実装コード

 「<GetEnumerator>d__0」クラスのコンストラクタでは、そのパラメータで渡された「<>1__state」という変数をフィールド変数として保存している。

 次に、MoveNextメソッドを見てみよう。なおここまでの内容により、「<>1__state」フィールド変数には「0」が入っている。

bool MoveNext()
{
  switch ( <>1__state ) {
    case 0: goto IL_001f;
    case 1: goto IL_0070;
    default: goto IL_0096;
  }

// 「<>1__state == 0」のとき
IL_001f:
  <>1__state = -1;
  <i>5__1 = 1; // for( i=1;i<=3;i++ ) {
  goto IL_0086

IL_002f:
  Console.WriteLine( "yield return {0}; ThreadId={1}",
    <i>5__1, System.Threading.Thread.CurrentThread.ManagedThreadId );
  <>2__current = <i>5__1;  // yeild return i;
  <>1__state = 1;
  return true;

// 「<>1__state == 1」のとき
IL_0070:
  <>1__state = -1;
  <i>5__1 ++; // end of for

IL_0086:
  if( <i>5__1 <= 3 ) goto IL_002f;

IL_0096:
  return false;
}
「<GetEnumerator>d__0」クラスのMoveNextメソッドの実装コード

 MSILコードには例えばfor文などがないため、switch文やif文、goto文などを使ってすべてを表現するしかない。よって、上記コードも少し読みづらいが、赤い文字の部分がイテレータの機能を実現している部分だ。

 さて、「yield return」文がどのようなMSILコードになったのかを確認しよう。まず、「<>2__current」というフィールド変数にローカル変数「i」の値を代入することで、Currentプロパティの値を設定している。続いてフィールド変数「<>1__state」に「1」を代入することで、次回の呼び出し時に「IL_0070」というラベル部分が開始点になるようにしている。ここでtrueを返しているということは、まだ「yield return」文により戻す値が残っていることを示している。

 各開始点でフィールド変数「<>1__state」に「-1」を代入しているので、forループを抜けた場合(つまり「IL_0096」というラベルへ移動した場合)には、ここで返す値はfalseになる。それ以後の呼び出しについては、switch文部分にあるdefault文でラベル「IL_0096」に導かれて何もせずにfalseをリターンするだけだ。

 以上がイテレータを実現している内側の仕組みだ。「yield return」文の数が増えたとしても、内部の状態を管理しているフィールド変数「<>1__state」の取る値のバリエーションが増えるだけである。

 単純にいえば、イテレータを使うとメソッド内の抜け出すポイントに番号が振られる。そして、どこに戻るべきかを管理するコード、戻るべき場所へ飛び込むためのコードが生成される。これにより関数の断片を実行しているような振る舞いが示されるのである。

 イテレータがどのように実現されているかを見た。次にイテレータを記述するうえでの注意点についても触れておこう。


 INDEX
  [特集] C# 2.0新機能徹底解説(前編)
  開発生産性を飛躍的に高めるジェネリック
    1.ジェネリックとは?
    2.ジェネリック・コレクションの利用
    3.汎用アルゴリズムを実装しているList<T>クラスのメンバ
    4.ジェネリックを使ったコレクション・クラスの拡張
  [特集] C# 2.0新機能徹底解説(後編)
  進化したC# 2.0の状態管理、匿名メソッドとイテレータ
    1.匿名メソッドとその正体
    2.イテレータの衝撃!
  3.イテレータを解剖する
    4.確実な後処理
 


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 記事ランキング

本日 月間