連載:[完全版]究極のC#プログラミング

Chapter6 ラムダ式(前編)

川俣 晶
2009/10/19

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

 一時期、C#の匿名メソッド(ラムダ式の前身となったC# 2.0の機能。本章「6.9 C# 2.0と匿名メソッド」参照)はクロージャ*1ではないという説が流布されたことがあるが、これは誤りである。

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

 ラムダ式を使うプログラマーならクロージャを知らない人にも関係する重要な話なので、実際に見てみよう。

 リスト6.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] = () => { return i; };
    }
    Console.WriteLine("{0} {1}", methods[0](), methods[1]());
    // 出力:2 2

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

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

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

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

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] = () => { return j; };
    }
    Console.WriteLine("{0} {1}", methods[0](), methods[1]());
    // 出力:0 1
  }
}
リスト6.7 forループでForEachメソッドと同じ結果を得るコード

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

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

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

 一方、変数jは違う。これはループ内部のスコープに入った時点で生成される変数である。それゆえ、2回のループを行えば、2個の別個の変数jが作り出される。2つのラムダ式からキャプチャされる変数jは、それぞれ別個のものである。

 したがって、2つのラムダ式が同じ値を返すとは限らない。

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

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

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

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


 INDEX
  [完全版]究極のC#プログラミング
  Chapter6 ラムダ式(前編)
    1.6.1 おかずでもデザートでもなく“ご飯”
    2.6.2 ラムダ式とは何か?/【C#olumn】定義済みデリゲートを活用しよう
    3.6.3 ラムダ式は上位スコープにアクセスできる
    4.6.4 キャプチャされる変数
  5.6.5 注意を要するキャプチャの本質
    6.6.6 デリゲートの共変性と反変性
    7.6.7 デリゲートインスタンスの等価性
    8.6.8 ラムダ式で継承を置き換えてみる
    9.6.9 C# 2.0と匿名メソッド/【Exercise】練習問題
 
インデックス・ページヘ  「[完全版]究極のC#プログラミング」


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

本日 月間