特集

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

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

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

 先ほどのMSILコードを基に(手動で)再現したPrintOrderReciptメソッドのC#のソース・コードは次のとおりだ。

class PurchaseOrder
{
  class <>c__DisplayClass1
  {
    public decimal TotalAmount;
    public decimal TotalPrice;
    public void <PrintOrderRecipt>b__0( OrderLine ol )
    {
      System.Console.WriteLine("{0} {1} {2} {3}",
           ol.prod.Name, ol.prod.Price, ol.amount, ol.prod.Price * ol.amount);
      TotalPrice += ol.prod.Price * ol.amount;
      TotalAmount += ol.amount;
    }
  }

  public void PrintOrderRecipt()
  {
    <>c__DisplayClass1 local0 = new <>c__DisplayClass1();
    local0.TotalAmount = 0;
    local0.TotalPrice = 0;
    Items.ForEach( local0.<PrintOrderRecipt>b__0 );
    System.Console.WriteLine("合計 {0} {1}", local0.TotalAmount, local0.TotalPrice);
  }
}
MSILコードから再現したPrintOrderReciptメソッド

 C#では「<」や「>」は関数名の一部として使えないので、上記コードはコンパイルできないが、何が行われているかは十分に伝わるだろう。

 匿名メソッドを利用すると、メソッドのローカル変数はクラス内に定義された内部クラスのフィールド変数に変換される。そして、匿名メソッドの本体は内部クラスのメソッドとして記述される。ローカル変数へのアクセスは透過的に、内部クラスのフィールド変数へのアクセスと変わるわけだ。

■匿名メソッドのまとめ

 匿名メソッドの正体を突き止めた結果が示すものは、「匿名メソッドを作ること=小規模なクラスを作ること」であるということだ。

 匿名メソッドはデリゲートを記述するための仕組みではなく、クラスを記述するための仕組みである。つまり、匿名メソッドで簡単にデリゲートが実装できるというのは、単に「デリゲートとなるメソッドのエントリポイント(=メソッドが呼び出される場所)がソース・コード上で名前を持たなくてもよくなる」というC#言語の便宜的な仕様でしかない(実際にはクラスを記述しているのだ)。

 もちろんだからといって、デリゲートが簡単に書けるというメリットがなくなるわけではない。むしろ、デリゲートを記述する手段として大いに利用すべきだろう。実際に、前編で示したジェネリックのサンプル・コードでも、デリゲートの実装に匿名メソッドを多用している。

 デリゲートに対して匿名メソッドを利用するとよい例を挙げてみよう。例えば、ジェネリックのList<T>コレクションのComparison<T>デリゲートは並べ替えの条件を表す。よってComparison<T>デリゲートの実装個所がSortメソッドの呼び出しに近ければ近いほどコードは理解しやすくなる。そこで匿名メソッドによりComparison<T>デリゲートの内容を記述して、それをSortメソッドのパラメータに直接埋め込めばその距離を最短に抑えることができる。

 逆に匿名メソッドを使うべきでないケースも多々ある。例えば、匿名メソッドの内容がほかのメソッドの内容と重複するのであれば、匿名メソッドをクラスか一般的なメソッドにリファクタリングした方がよいかもしれない。これは、匿名メソッドではその内容を再利用できないからだ。

 匿名メソッドにはメソッド名がないために、それが記述されている場所以外からアクセスできないという状況が生まれる。このため、その処理内容を再利用しようとして、コードのコピーという悪魔の誘いに乗りやすい。コードのコピーは、問題点を拡散させるという最悪の結果を生み出すので、早いうちにその芽を摘むことが重要だ。

 しかし、あなたの所属しているプロジェクトが「動いたものはいじるな」というポリシーを持っているのであれば、リファクタリングの道は閉ざされている。匿名メソッドの処理内容を再利用する可能性がある場合には、その処理がどこからも呼べるように、メソッドには名前を付けるべきであり、匿名メソッドは使うべきではない。

イテレータの衝撃! C#は構文の意味を理解する言語に進化したのか?

 コンパイラはもともと構文の意味を理解する。そういう指摘もあるだろうが、本章に上記のようなタイトルを付けたのには理由がある。またこのタイトルの最後に「?」が付いているのにも理由がある。

 まず、イテレータについて簡単に説明しておこう。

 イテレータは、IEnumeratorオブジェクトを返すGetEnumeratorというメソッドを実装するための機能だ。GetEnumeratorメソッド内では「yield return」文によって複数の値を順次戻り値として返却することが可能である。これらの複数の戻り値は、GetEnumeratorメソッドの呼び出し元であるforeach文において、ループのたびに1つずつ得ることができる。

 細かいことはコードを見てもらった方が早いだろう。以下がそのサンプル・コードだ。Visual Studio 2005のベータ版を持っている人は以下のコードを実行して、どのような結果になるか試してみてほしい。

using System;
using System.Collections;
using System.Threading;
class OneTwoThree
{
  public IEnumerator GetEnumerator()
  {
    for ( int i=1;i<=3;i++ ) {
      Console.WriteLine( "yield return {0}; ThreadID={1}",
        i,Thread.CurrentThread.ManagedThreadId );
      yield return i;
    }
  }
}

class Program
{
  public static void Main( string[] args )
  {
    OneTwoThree iter = new OneTwoThree();
    foreach ( int i in iter ) {
      Console.WriteLine( "Main: value={0} ThreadID={1}",
        i,Thread.CurrentThread.ManagedThreadId );
    }
  }
}
イテレータの実行スレッドを確認するサンプル・コード

 GetEnumeratorメソッドでは、1〜3までの複数の戻り値が「yield return」文で返される。そして、これらの戻り値は、foreach文のループで1つずつ取得できる。

 このコードの実行結果は以下のようになる(ThreadIdの数値は実行環境による)。

yield return 1; ThreadID=9
Main: value=1 ThreadID=9
yield return 2; ThreadID=9
Main: value=2 ThreadID=9
yield return 3; ThreadID=9
Main: value=3 ThreadID=9
イテレータのサンプル・プログラムの実行結果

 この実行結果から、GetEnumeratorメソッド内での出力とMainメソッド内での出力が交互に現れているのが見て取れる。そして、そのThreadIdの値(上記の結果では「9」となっている)は一致している。ここで疑問点がいくつも浮かび上がってくるだろう。

 1つのスレッドがMainメソッドとGetEnumeratorメソッドを同時に実行したということであろうか。そんなことはあり得ない。スレッドが1個であれば実行できるメソッドは1カ所しかないはずだ。だとすると、どうしてこのような動きになるのか。

 この実行結果を見ると、イテレータを使って記述したコードのそれぞれの意味は維持されているが、C#のコンパイラはそれをばらばらに分解し、分解された各断片をそれぞれ実行しているような印象を受ける。C#のコンパイラは構文の意味を理解していて、その意味を実現する断片のコードを自動的に生成しているということなのだろうか。

 このような疑問が本章のタイトルにつながってくる。この疑問に対する答えを得るには、イテレータの内側の仕組みを調べてみる必要がありそうだ。続いて、それを調べてみよう。


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

本日 月間