[C#] ラムダ式内でラムダ式外のループカウンタ変数を使用すると危険

Teratailで同じような原因の質問が別々の方からあったのでメモ代わりに。
[C#のTask配列をforでセットしようとするとエラーになる](https://teratail.com/questions/307086)
[基本的な質問ですが、Taskの結果がバラバラになります](https://teratail.com/questions/318485)

#一見普通に動きそうなコードだけど…
さて、まずは下記のサンプルコードを見てください。

```CS
    Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [for Start]");
    for (var i = 1; i <= 5; i++)
    {
        Task.Run(async () =>
        {
            await Task.Delay(1000);
            Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [Task End({i})]");
        });
    }
    Console.ReadLine();
```

forループで実行されたTask内で、1秒待ってループカウンタ i をコンソール出力しているだけのコードのように見えますね。これを見て、皆さんはどういう実行結果を想像しますか？
Task End(1～5)が出力されるだけだろ？と思う人は結構いるかもしれません。しかし、実際は…？

```:実行結果
10:43:01.801 [Start for]
10:43:02.928 [Task End(6)]
10:43:02.928 [Task End(6)]
10:43:02.928 [Task End(6)]
10:43:02.928 [Task End(6)]
10:43:02.928 [Task End(6)]
```
なんてこった！全部Task End(6)になっています。一体どういう事なんだ…

#何故ループカウンタを直接使用してはいけないのか
先程のサンプルコードで出力結果が全部Task End(6)になった理由は単純で、非同期Taskが実行されて、実際にコンソール出力するタイミングの時点で、ループカウンタは既に6までカウントアップされてループを終えているからです。**ラムダ式外のループカウンタを直接使用した場合、いつの時点でのループカウンタの値が使用されているか判らない**のです。

ちなみに、VB.NETで同等の処理を行った場合、ビルド時にwarningが発生します。C#でも出してくれてもいいのでは？という気はしないでもないです。
([BC42324](https://docs.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/error-messages/using-the-iteration-variable-in-a-lambda-expression-may-have-unexpected-results)より抜粋)

> warning BC42324: ラムダ式内で繰り返し変数を使用すると、予期しない結果が発生する可能性があります。代わりに、ループ内にローカル変数を作成して繰り返し変数の値を割り当ててください。

今度はforループ内でローカル変数countを宣言してループカウンタを割り当て、その値をコンソール出力するようにしてみましょう。

```CS:ループ内ローカル変数使用版サンプル
   Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [Start for]");
    for (var i = 1; i <= 5; i++)
    {
        var count = i;
        Task.Run(async () =>
        {
            await Task.Delay(1000);
            Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [Task End({count})]");
        });
    }
    Console.ReadLine();
```

```:実行結果（ループ内ローカル変数使用）
11:17:34.883 [Start for]
11:17:36.028 [Task End(1)]
11:17:36.028 [Task End(4)]
11:17:36.028 [Task End(5)]
11:17:36.028 [Task End(3)]
11:17:36.028 [Task End(2)]
```
1～5がバラバラに出力されましたね。forループ内でローカル変数を宣言する事で、ループ毎に変数が確保され、ループカウンタの値が保持されています。ちなみに、実際にこの手の並列処理を行う場合は、[Parallelクラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.parallel?view=net-5.0)というものがあるので、そちらを利用した方がよいでしょう。

```CS:Parallel.For使用版サンプル
    Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [Start Parallel.For]");
    Parallel.For(1, 6, async (i) =>
    {
        await Task.Delay(1000);
        Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [Task End({i})]");
    });
    Console.ReadLine();
```

```:実行結果（Parallel.For使用）
11:30:34.820 [Start Parallel.For]
11:30:35.966 [Task End(2)]
11:30:35.966 [Task End(1)]
11:30:35.966 [Task End(4)]
11:30:35.966 [Task End(3)]
11:30:35.967 [Task End(5)]
```

#Task.Runをawaitすれば良いのでは？
そういう感じの回答もありましたが、単なる同期処理になりTask使う意味が無くなりますので、それならむしろTaskを使用しない方が良いでしょう。さて、実際にawaitさせてみましょう。

```CS:await使用版サンプル
    Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [Start for]");
    for (var i = 1; i <= 5; i++)
    {
        await Task.Run(async () =>
        {
            await Task.Delay(1000);
            Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss.fff")} [Task End({i})]");
        });
    }
    Console.ReadLine();
```

```:実行結果（await使用）
11:57:27.309 [Start for]
11:57:28.394 [Task End(1)]
11:57:29.403 [Task End(2)]
11:57:30.420 [Task End(3)]
11:57:31.427 [Task End(4)]
11:57:32.437 [Task End(5)]
```

Task End(1～5)が出力されていますが、普通に非同期で実行すれば1秒で終わる処理が順次同期実行しているので、全体で5秒掛かっています。

#参考URL
[ループをParallelクラスで並列処理にするには？［C#／VB］](https://www.atmarkit.co.jp/ait/articles/1706/21/news021.html)
[データとタスクの並列化における注意点](https://docs.microsoft.com/ja-jp/dotnet/standard/parallel-programming/potential-pitfalls-in-data-and-task-parallelism)