特集

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

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

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

 前編では、C# 2.0の3つの主要な新機能のうち、ジェネリック(Generics)についての概要を解説した。後編である今回は、残る2つの新機能、

  • 匿名メソッド(Anonymous Methods)
  • イテレータ(Iterator)

について解説を行っていく。

 匿名メソッドとイテレータはどちらも、状態管理に必要なロジックをプログラマに代わって生成してくれるという特徴を持っている。ここでいう状態管理とは、スコープを越えたローカル変数の管理や、foreach文によるループ処理内部での実行ポイントの管理のことである(詳細は追って説明する)。

 特にイテレータは、複雑な状態管理をコンパイラにより生成されるMSILコードが自動的に行ってくれる。これにより、単純なコードでも複雑な状態を表現できるようになる。その変化は衝撃的だ。今回はその変化の仕組みの内側を探ることで、C# 2.0が持っている状態管理の全容を解き明かしてみよう。

匿名メソッド

 匿名メソッドは、前編のコード例でも示したように「デリゲートの実装を簡単に行うための機能」と一般的には解説されている。確かにそれは間違いではないが、匿名メソッドが実現できる全機能を表現しているとはいえない。匿名メソッドの持つ「親スコープとの状態の共有」(詳細後述)はもっと深い側面を持っているのだ。

 ここでは匿名メソッドのサンプル・コードを示すために、前編で作成したジェネリックのサンプル・コードを流用して、明細の印刷処理を行うPrintOrderReciptメソッドを新たに作成する。このメソッドでは例として分かりやすいようにプリンタではなくコンソールに出力するようにした。そのコードは以下のとおりだ。

public void PrintOrderRecipt()
{
  decimal TotalPrice = 0;
  decimal TotalAmount = 0;
  // 匿名メソッドとして記述されたコード
  Items.ForEach(delegate(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;
    }
  );
  System.Console.WriteLine("合計 {0} {1}", TotalAmount, TotalPrice);
}
匿名メソッドのサンプル・コード

 このコードには、次の2つのメソッドが含まれている。

(1)PrintOrderReciptメソッド(外側の「public void PrintOrderRecipt(){ … }」の部分)
(2)匿名メソッド(コレクションのForEachメソッドのパラメータに指定されている「delegate(OrderLine ol) { … }」の部分)

 これまでのC#では、(2)の内容を独立したメソッドとして記述する必要があった。しかし、匿名メソッドを使うと、メソッド名を持った1つのメソッドとして記述することなく、そのメソッドが必要となる場所に処理内容を直接記述することできる。イベント・ハンドラなどの、デリゲートを経由して呼び出すメソッドは、その呼び出し元が1カ所のみの場合が多く、そのためにデリゲートの宣言とメソッドの記述を省略できるわけだ。

 ところで、上記のコードにおいて1つ不思議な点に気付いただろうか。TotalPriceとTotalAmountの2つのローカル変数はPrintOrderReciptメソッドのスコープにあるので、本来異なるメソッドである匿名メソッド内(「delegate(OrderLine ol) { … }」の部分)からはアクセスできないはずだ。しかし、匿名メソッドではそれが可能なのだ。

 このように「匿名メソッドの親スコープ(この例では、PrintOrderReciptメソッドのスコープ)と状態(この例ではローカル変数TotalPriceとTotalAmount)を共有できる」ということが、匿名メソッドがデリゲートの記述を簡単に行うためだけの仕組みではないと筆者が説明する理由だ。

 本来アクセスできないものを一体どのようにアクセスさせているのか。 その仕組みを解き明かすために、このメソッドをILDASM(ildasm.exe。MSILのコードを出力する逆アセンブラ)で見てみよう。

■ILDASMで見る匿名メソッドの正体

 匿名メソッドの仕組みは、匿名メソッドを使わないときの仕組みと比べると理解しやすいだろう。そこで、先ほどのサンプル・コードのItems.ForEachメソッドの部分をforeachキーワードを使って書き直した以下のコードと比較してみる。

public void PrintOrderRecipt2()
{
  decimal TotalPrice = 0;
  decimal TotalAmount = 0;

  foreach( OrderLine ol in Items )
  {
    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;
  }
  System.Console.WriteLine("合計 {0} {1}", TotalAmount, TotalPrice);
}
ForEachメソッドをforeachキーワードに置き換えた匿名メソッドのサンプル・コード

 メソッド名をPrintOrderRecipt2として前述のサンプル・コード(PrintOrderReciptメソッド)と区別が付けられるようにしている。

 これらをそれぞれILDASMで逆アセンブルし、メソッドの冒頭部分の「.locals」までを比較してみよう。

.method public hidebysig instance void  PrintOrderRecipt() cil managed
{
  // コード サイズ       88 (0x58)
  .maxstack  4
  .locals init ([0] class ConsoleApplication3.PurchaseOrder/'<>c__DisplayClass1' '<>8__locals2')
PrintOrderReciptメソッド冒頭部分(匿名メソッドを利用)のMSILコード
 
.method public hidebysig instance void  PrintOrderRecipt2() cil managed
{
  // コード サイズ       259 (0x103)
  .maxstack  5
  .locals init ([0] valuetype [mscorlib]System.Decimal TotalPrice,
           [1] valuetype [mscorlib]System.Decimal TotalAmount,
           [2] valuetype ConsoleApplication3.PurchaseOrder/OrderLine ol,
           [3] valuetype [mscorlib]System.Collections.Generic.'List`1'/Enumerator<valuetype ConsoleApplication3.PurchaseOrder/OrderLine> CS$5$0000,
           [4] object[] CS$0$0001,
           [5] bool CS$4$0002)
PrintOrderRecipt2メソッド冒頭部分(foreachキーワードを利用)のMSILコード

 匿名メソッドを使った場合にはローカル変数としては1個だけ、定義した覚えのないクラスの変数「<>8__locals2」を確保している。変数の型の部分「ConsoleApplication3.PurchaseOrder/’<>c__DisplayClass1’」の「PurchaseOrder」と「<>c__DisplayClass1」の間の「/」は、PurchaseOrderクラス内にネストした入れ子クラス「<>c__DisplayClass1」が定義されていることを示す。ILDASMの画面を見てみよう。

ILDASMの実行画面
匿名メソッドでは、クラスが暗黙的に自動生成される。

 確かに「<>c__DisplayClass1」というクラスが作成されている。そしてローカル変数であったTotalAmountとTotalPriceは、そのクラスのフィールド変数になっているのが見て取れるだろう。このように匿名メソッドは、実は(暗黙的な)クラスを作り出すための機能なのだ。

 この場合のデリゲートの実行するメソッドは「<>c__DisplayClass1」クラスの「<PrintOrderRecipt>b__0」メソッドになっている。しかしMSILコードのままでは、少し分かりにくい。そこで、このMSILコードを基にPrintOrderReciptメソッドの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 記事ランキング

本日 月間