読者です 読者をやめる 読者になる 読者になる

雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

ラムダ式の変数スコープ

C#

興味深く読んでます。
……が、なんか「続続」の方の例がいまいちな様な気がします。
そもそも、ラムダ式に於ける変数スコープの扱いは、 MSDN によると、

  • 取り込まれた変数は、それを参照するデリゲートがスコープ外に出るまでガベージ コレクトされません。
  • ラムダ式内に導入された変数は、外側のメソッドでは参照できません。
  • ラムダ式は、外側のメソッドの ref パラメータまたは out パラメータを直接取り込むことはできません。
  • ラムダ式に含まれる return ステートメントで外側のメソッドを戻すことはありません。
  • ラムダ式は、その本体の外部または含まれている匿名関数本体内をジャンプ先とする goto ステートメント、break ステートメント、continue ステートメントを含むことはできません。
http://msdn.microsoft.com/ja-jp/library/bb397687.aspx

とされています。
割と普通さんのところで話題にしているのは、この中の一番上の話だと思うのですが、「続続」に書かれている 2 つの例は、どちらもこれに該当していないと思われます。
以下、検証。

一つ目の例(かるあさんが提示されたもの)

まずは例のコピー。

IList<Action> list3 = new List<Action>();
for (int i = 0; i < 3; i++)
{
    list3.Add(((Func<int, Action>)(k => () => Console.WriteLine(k)))(i));
}
foreach (var func in list3)
{
    func();
}
// 0
// 1
// 2

この例で重要なのは、やはり list3.Add() の部分ですね。
判別しやすいように切り出してインデントをつけてみます。

list3.Add(                                // [1]
    (                                     // [2]
        (Func<int, Action>)               // [3]
            (k =>                         // [4]
                () =>                     // [5]
                    Console.WriteLine(k)  // [6]
            )                             // [7]
    )(i)                                  // [8]
);

ここで、[4] 〜 [7] は「 int 型の引数」を「ひとつ」とって「引数なしの Action デリゲート」を返す Func デリゲートになっています。( [3] は明示的な型キャスト)
そして、 [8] でその Func デリゲートに対して「 i 」を引数として実行しています。
i は外側のループ変数なので、 i = 0 to 2 について、それぞれ上記処理が実行されます。
具体的に、 i = 0 の場合を見てみると、

list3.Add(                                // [1]
    (                                     // [2]
        (Func<int, Action>)               // [3]
            (k =>                         // [4]
                () =>                     // [5]
                    Console.WriteLine(k)  // [6]
            )                             // [7]
    )(0)                                  // [8]
);

なので、 Func デリゲートを「 0 」を引数として実行した結果( Action デリゲート)が list3 に追加されます。

list3.Add((Action)( () => Console.WriteLine(0) ));

……この結果で判ると思いますが、 list3 に追加される時点で、変数は全て展開されてしまっています。
つまり、これでは変数のスコープ確認にならないです。。。
以上、ひとつめの検証、終わり。

二つ目の例

var num = 0;
Func<int> f = () => num;
num += 10;
Console.WriteLine(f());
// 10

これはかるあさんがコメントで言っている通り、デリゲートの遅延実行の動きですね。

二つ目の例の変形

二つ目の例をちょっと書き換えて、スコープの確認例を作ってみました。

class LambdaCheck {
  Func<int> func;

  public void Initialize() {
    var num = 0;
    func = () => num;
    num += 10;
  }                                  // ここで num のスコープは終了する

  public void Execute() {
    Console.WriteLine(func());       // デリゲート実行
                                     // Initialize() 内のローカル変数である num にアクセス
                                     // 「 10 」が出力される
  }

  static void Main() {
    var lambda = new LambdaCheck();
    lambda.Initialize();             // f の初期化
    lambda.Execute();                // 着火
  }
}

この例だと、 num のスコープ外で func が実行されますが、それでも num の参照は可能であることが判ります。

複数のラムダ式で変数を共有する

かるあさんのコメントにあった話です。
こんな感じになるんでしょうか。

class LambdaCheck2 {
    Func<int> func1;
    Func<int> func2;
    Func<int> func3;

    public void Initialize() {
        var num = 0;
        func1 = () => { num += 2; return num; };
        func2 = () => { num += 4; return num; };
        func3 = () => { num += 8; return num; };
        num += 1;
    }

    public void Execute() {
        Console.WriteLine(func3()); // 9
        Console.WriteLine(func1()); // 11
        Console.WriteLine(func2()); // 15
    }

    static void Main() {
        var lambda = new LambdaCheck2();
        lambda.Initialize();
        lambda.Execute();
    }
}

スコープ外でもちゃんと共有出来るみたいですね。