Unity & C# 学習教材

変数キャプチャ

ラムダ式は、自分が定義されたスコープの変数を「取り込んで」使えます。この動作を変数キャプチャ(またはクロージャ)と呼びます。便利な反面、ループ内で使うときにはまりやすい落とし穴があります。

学習目標

このページを読み終えると、以下のことができるようになります。

前提知識


1. 変数キャプチャとは

通常のメソッドは、自分のパラメータとローカル変数しか使えません。ラムダ式は、それに加えて定義された時点で見えていた外側の変数もそのまま使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;

public class Program
{
    public static void Main()
    {
        string greeting = "こんにちは";

        Action<string> greet = name => Console.WriteLine($"{greeting}{name}!");

        greet("Alice");

        greeting = "おはよう";     // 外側の変数を変更
        greet("Bob");              // ラムダ式は変更後の値を参照する
    }
}
1
2
こんにちは、Alice!
おはよう、Bob!

greeting はラムダ式の外で定義されていますが、ラムダ式の中から参照できています。また、greeting を書き換えた後に greet("Bob") を呼ぶと、更新後の値 "おはよう" が使われます。これはラムダ式がコピーではなく変数そのものを保持しているためです。


2. キャプチャはコピーではなく参照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

public class Program
{
    public static void Main()
    {
        int count = 0;

        Action increment = () => count++;

        increment();
        increment();
        increment();

        Console.WriteLine(count);   // 外側の count が変更されている
    }
}
1
3

count++ はラムダ式の外の count を直接インクリメントしています。ラムダ式が count をコピーしているのではなく、元の変数を共有していることがわかります。


3. for ループ内のキャプチャの罠

ループ変数をキャプチャするときに、意図しない動作になりやすい罠があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        var actions = new List<Action>();

        for (int i = 0; i < 3; i++)
        {
            actions.Add(() => Console.WriteLine(i));
        }

        foreach (var action in actions)
        {
            action();
        }
    }
}
1
2
3
3
3
3

ループが完了すると i の値は 3 になっています。3 つのラムダ式がすべて同じ変数 i を参照しているため、呼び出し時点の値(3)が表示されます。

ループごとに値を固定するには、ループ内に新しい変数を作ってキャプチャします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        var actions = new List<Action>();

        for (int i = 0; i < 3; i++)
        {
            int captured = i;   // ループ毎に新しい変数を作る
            actions.Add(() => Console.WriteLine(captured));
        }

        foreach (var action in actions)
        {
            action();
        }
    }
}
1
2
3
0
1
2

captured はループの各イテレーションで新しく生成されるため、それぞれ異なる変数をキャプチャします。

💡 ポイント: foreach のループ変数は C# 5 以降、各イテレーションで新しい変数として扱われるためこの問題は起きません。for ループの int i に注意してください。


4. static ラムダ

C# 9 から、ラムダ式に static を付けると外側の変数や this をキャプチャしようとしたときにコンパイルエラーにできます。意図せずキャプチャが発生するのを防ぎたい場面で使います。

書式:static ラムダ

1
static (パラメータ) => 式
要素 説明
static キャプチャを禁止するキーワード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

public class Program
{
    public static void Main()
    {
        int multiplier = 3;

        // ❌ NG: static ラムダで外側の変数をキャプチャしようとするとコンパイルエラー
        // Func<int, int> badLambda = static x => x * multiplier;

        // ✅ OK: パラメータだけを使う
        Func<int, int, int> multiply = static (x, factor) => x * factor;

        Console.WriteLine(multiply(5, multiplier));
    }
}
1
15

よくあるミス

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ NG: for ループ変数を直接キャプチャすると、全ラムダが同じ変数を参照する
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));   // 全部 3 になる
}

// ✅ OK: ループ内で新しい変数にコピーしてキャプチャする
for (int i = 0; i < 3; i++)
{
    int captured = i;
    actions.Add(() => Console.WriteLine(captured));   // 0, 1, 2 になる
}

まとめ


理解度チェック

以下の問いに答えられるか確認しましょう。

  1. ラムダ式がキャプチャした変数は、コピーですか、参照ですか?それによって何が起きますか?
  2. 次のコードの出力結果は何になりますか?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main()
        {
            var actions = new List<Action>();
    
            for (int i = 0; i < 4; i++)
            {
                int n = i * 2;
                actions.Add(() => Console.WriteLine(n));
            }
    
            foreach (var a in actions) a();
        }
    }
    
  3. (応用)上記コードで int n = i * 2; の行を削除し、代わりに () => Console.WriteLine(i * 2) と書いた場合、出力結果はどう変わりますか?理由も答えてください。
解答を見る
  1. 参照です。キャプチャした変数が後から書き換えられると、ラムダ式が次に呼ばれたときに新しい値が使われます。

  2. 1
    2
    3
    4
    
    0
    2
    4
    6
    

    各イテレーションで n という新しい変数が作られるため、それぞれ 0, 2, 4, 6 がキャプチャされます。

  3. 全て 8 が出力されます(i の最終値 4 に対し 4 * 2 = 8)。ループ終了後に i4 になっており、3 つのラムダが同じ i を参照しているためです。

次のステップ

ローカル関数 では、メソッドの内側に定義できるメソッドと、ラムダ式との使い分けを学びます。