連載:C# 2.0入門

第5回 匿名メソッドとデリゲート

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

匿名メソッドは上位スコープにアクセスできる

 匿名メソッドが名前のあるメソッドと決定的に違うのは、実は名前がないことではなく、上位スコープにアクセスできることである。

using System;

delegate void MyAction();

class Program
{
  static void Main(string[] args)
  {
    string message = "Hello! World!";
    MyAction action = delegate()
    {
      Console.WriteLine(message); // messageは上位スコープの変数
    };
    action(); // 出力:Hello! World!
  }
}
リスト4 文字列を引数で渡さなかった場合

 この場合、変数messageはMainメソッドに属する変数であり、匿名メソッドには属していない。しかし、匿名メソッドは上位スコープにアクセスできるので、変数messageの内容を出力することができる。

 この特徴は非常に重要である。なぜなら、必要とされるあらゆる情報を引数経由で渡さずに済むので、引数の肥大化が防げるのである。また、引数が最小限に絞られることで、少数のデリゲート型を使い回すだけで済んでしまうことも多くなる。

 まさに、この特徴ゆえに匿名メソッドは気楽に使える「ご飯」のような機能になっているわけである。

キャプチャされる変数

 上位スコープにアクセスできるというのは、実はいうほど簡単な話ではない。

 次のプログラムを見ていただきたい。常識的に考えればこのプログラムは動作しないはずである。なぜなら、変数messageはCreateMethodメソッドからリターンした瞬間に消滅するが、匿名メソッドが呼ばれるのはその後だからである。

using System;

delegate void MyAction();

class Program
{
  static MyAction CreateMethod()
  {
    string message = "Hello! World!";
    return delegate()
    {
      Console.WriteLine(message);
    };
  }

  static void Main(string[] args)
  {
    MyAction action = CreateMethod();
    action(); // 出力:Hello! World!
  }
}
リスト5 消滅した変数を参照した例

 しかし、このプログラムは正常に動作する。なぜかといえば、「外部変数のキャプチャ」という機能が用意されているからだ。

 外部変数のキャプチャとは、匿名メソッドが上位スコープの変数を参照しているとき、その変数の寿命を匿名メソッドの寿命が尽きるまで延命する機能である(より正確にいえば、C# 2.0言語仕様で「少なくとも、匿名メソッドを参照するデリゲートがガベージ コレクションできる状態になるまで延長されます」と記述されている)。

 この機能があるため、本来ならメソッドの実行が終わった時点で寿命が尽きたはずの変数messageは、匿名メソッドから出力されたときにも生き延びていたのである。

注意を要するキャプチャの本質

 一時期、C# 2.0の匿名メソッドはクロージャ*1ではないという説が流布されたことがあるが、これは誤りである。

*1 クロージャとは、WikiPediaより引用すると「プログラミング言語において引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決する関数」のことである。

 しかし、このような誤解が生じた理由には注意を払う価値がある。正しい理解を行っていないと、キャプチャが思いどおりに機能しないというリスクの存在を示すからである。

 匿名メソッドを使うプログラマーなら、クロージャを知らない人にも関係する重要な話なので、実際に見てみよう。リスト6は、変数iに0と1の数字を格納しつつ匿名メソッドを作成するコードを2つ含んでいる。1つはfor文、もう1つはForEachメソッドでループを回しているという点が異なる。

using System;

delegate int SampleMethodDelegate();

class Program
{
  static void Main(string[] args)
  {
    SampleMethodDelegate[] methods = new SampleMethodDelegate[2];

    // シンプルなforループ
    for (int i = 0; i < 2; i++)
    {
      methods[i] = delegate() { return i; };
    }
    Console.WriteLine("{0} {1}", methods[0](), methods[1]());
    // 出力:2 2

    // ForEachメソッドを使う
    int[] array = { 0, 1 };
    Array.ForEach(array, delegate(int i)
    {
      methods[i] = delegate() { return i; };
    });
    Console.WriteLine("{0} {1}", methods[0](), methods[1]());
    // 出力:0 1
  }
}
リスト6 同じ結果を示さない匿名メソッドの例

 見てのとおり、結果は同じにならない。

 for文でループした方は、どちらのメソッドを呼び出しても2という結果しか得られない。しかし、ForEachメソッドでループした方は0と1という匿名メソッド作成時の値を覚えていて、それを出力してくれる。

 だが、これを見て「ForEachメソッドって賢いんですね!」と思うのは早計である。なぜかといえば、ほんの少し書き直すだけ、for文によるループでも同じ結果が得られるためだ。

using System;

delegate int SampleMethodDelegate();

class Program
{
  static void Main(string[] args)
  {
    SampleMethodDelegate[] methods = new SampleMethodDelegate[2];

    // シンプルなforループ
    for (int i = 0; i < 2; i++)
    {
      int j = i;
      methods[i] = delegate() { return j; };
    }
    Console.WriteLine("{0} {1}", methods[0](), methods[1]());
    // 出力:0 1
  }
}
リスト7 forループでForEachメソッドと同じ結果を得るコード

 見てのとおり、デリゲート生成時の値を覚えていて、0と1を出力している。果たして、何が違うのだろうか?

 リスト6のforループでは、変数iを使っているが、リスト7では一度、変数jにコピーしてからそれを利用している。この差は一見意味がないかのように見えるかもしれない。だが、キャプチャ機能から見ると、その差は大アリなのだ。

 まず、変数iについて見てみよう。この変数はfor文の実行が開始される際に1つだけ作られる。その結果、この1つの変数は2つの匿名メソッドからキャプチャされる。2つの匿名メソッドが読み書きする変数iは同じものである。それ故に、2つの匿名メソッドは同じ値を返す。

 一方、変数jは違う。これはループ内部のスコープに入った時点で生成される変数である。それ故に、2回のループを行えば、2個の別個の変数jが作り出される。2つの匿名メソッドからキャプチャされる変数jは、それぞれ別個のものである。それ故に、2つの匿名メソッドが同じ値を返すとは限らない。

 この差が、決定的な出力差を生むわけである。

 ここまでくれば、ForEachメソッドの方の動作も理解できるだろう。ForEachメソッドを使った場合のiは変数ではなく引数である。それ故に、メソッド呼び出しを行うごとに新しいものが確保される。つまり、2回の匿名メソッド呼び出しで使われる変数iは共有されていないのである。だから、異なる値を出力できるというわけである。

 変数が作り出される正確な位置とタイミングを理解しないと、キャプチャはプログラマーを裏切ることがある。注意しよう。


 INDEX
  C# 2.0入門
  第5回 匿名メソッドとデリゲート
    1.おかずでもデザートでもなくご飯/匿名メソッドとは何か?
  2.上位スコープのアクセス/キャプチャされる変数/キャプチャの本質
    3.引数を省略した匿名メソッド/共変性と反変性/インスタンスの等価性
    4.匿名メソッドで継承を置き換えてみる
 
インデックス・ページヘ  「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 記事ランキング

本日 月間