LINQのSelectは「遅延」するので使い方に気をつけるべし

LINQのSelect関数は、配列などの`IEnumerable<T>`オブジェクトの要素一つ一つについて、指定した処理を施した結果を別の`IEnumerable<T>`として返してくれる。

```csharp
using System.Collections.Generic;
using System.Linq;

void Func() {
    var list = new List<int>{1, 2, 3, 4, 5};

    foreach (var item in list.Select(i => i * i)) {
        System.Console.WriteLine(item); // 1, 4, 9, 16, 25 が１行ずつで表示される
    }
}
```

見落としがちなのが、このSelectで施そうとする処理は、実際に結果となるイテレート可能なオブジェクトが利用されるまで、**実行が遅れる**ということである。

```csharp
void Func() {
    var list = new List<int>{1, 2, 3, 4, 5};

    var sqList = list.Select(i => i * i); // この時点で、i * i は「計算されていない」
}
```

この例だと、Selectで作ったものはただの数値なのでSelectの実行が遅れてもさほど問題にはならないだろう。  
問題になるのは、要素からクラスを作ろうとする時だ。

# バグにつながる使い方

この例はUnity3D (UniTask) での利用をあげているが、そのほかのasyncのやり方でも同じことが考えられる。

あるクラスを`new`するときに重い処理が必要なので、コンストラクタから`async`コンテクストでその重い処理を行わせる指示だけをしておいて、準備ができたらメンバ変数のフラグを`true`にすることを考えよう。そして、そのクラスを別の配列の値からSelectを経由して作り、全てのクラスインスタンスから「準備完了」が返ってくるのを待つ処理を考える。

インターネット経由で指定したURLにある大きなデータを取ってくるという目的で、上記のような実装を考えるとこういったものになるだろう:

```csharp
public class FetchBigData {
    private bool _isReadyToRead = false;
    public bool IsReadyToRead { get {
            return _isReadyToRead;
        } 
    };
    
    private object _data;
    public object Data { get {
            return _data;
        }
    };

    public FetchBigData(string url) {
        FetchURL(url).Forget();
    }

    private async UniTask FetchURL(string url) {
        _data = await ...... // ダウンロード処理
        _isReadyToRead = true;
    }
}
```

そして、このクラスを複数のURLで作ってこのように処理完了を待つことを考える。

```csharp
void LoadManyGoneWrong() {
    var urls = new List<string>{"url1", "url2", "url3"};
    var fetchBigDataList = urls.Select(url => new FetchBigData(url)); // Uh oh

    // NG!!!
    await UniTask.WaitUntil(() => fetchBigDataList.All(i => i.IsReadyToLoad)); // 条件がfalseの間await
    foreach (var data in fetchBigDataList.Select(i => i.Data)) {
        // Do something about data
    }
}
```

残念ながら、この実装は `await` でソフトロックする。そればかりか、おそらく**サーバーに大量のリクエストが飛んで怒られる**羽目になる。

その理由は、**`Select`の実行が遅れる**ことにある。`fetchBigDataList`を作った`Select`では、URLを`FetchBigData`のコンストラクタに流して`FetchBigData`の配列を作ることになっている。この処理の実行が遅れているのだ。つまり、**この時点でFetchBigDataは作成されていない**。

遅れた処理は、それによって出来上がる要素が必要とされるまで実行されない。`WaitUntil`を評価しようとして`All`を実行して初めて`Select`が作る配列の中身が必要になるため、Selectの中身が実行されて`FetchBigData`が作成され、コンストラクタで呼んだダウンロード処理が発生する。  

ちなみに、UniTask.WaitUntilはそれに与えた条件がtrueになるまで、その条件を毎フレーム評価し続ける。ということは、`All`が`true`を返すまで、`All`は呼ばれ続ける。  
都合の悪いことに、これは**`Select`の中身まで毎フレーム呼ばれることを意味する**。つまりawaitに実行が進んだ時点で、**大量の`FetchBigData`が作られ続け、その時点でのダウンロード完了状態 == `false` が確認され、いつまで経っても実行が進まなくなる** ばかりか **`FetchBigData`からのリクエストが飛び続ける事故**が発生する。

# 回避方法

ではどうすればいいのかというと、`All`を打つ前に`Select`が作る配列の中身が必要な処理を実行してやれば良いのである。例えば`Tolist`だ。

```csharp
void CorrectlyLoadMany() {
    var urls = new List<string>{"url1", "url2", "url3"};
    var fetchBigDataList = urls.Select(url => new FetchBigData(url)).ToList(); // ToList()を打ち、強制的にSelectを実行させる

    await UniTask.WaitUntil(() => fetchBigDataList.All(i => i.IsReadyToLoad)); // 条件がfalseの間await
    foreach (var data in fetchBigDataList.Select(i => i.Data)) {
        // Do something about data
    }
}
```

このようにすれば、最初のSelectで生成した`FetchBigData`がその後も使い続けられ、無事に正しい数のリクエストが飛ぶこととなる。

# Select以外も遅延する

今回は`Select`を使ってハマったが、コメントでの指摘の通り、`Select`に限らず`IEnumerable<T>`が戻り値の型になっているLINQメソッドには同じような遅延実行が起こる(e.g. `Where` `GroupBy` `Take` `Skip`)。実は[公式ドキュメント](https://docs.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.where?view=net-5.0)の注釈にも書かれている。