IEnumerable<T>の遅延評価について(C#)

経緯

C#でIEnumerableを利用するときにReSharperで怒られた箇所が当初理解できなかったため、備忘録として残しておきます。

問題のコード

以下のようなコードを書いていたところ、「Possible multiple enumeration of IEnumerable.」と怒られました。

var items = GetItemQuery();

if(items.Count == 0)
// DoSomething

items.DoSomething()

原因

非常にわかりにくいのですが、以下の三点が原因でした。

  • varで隠蔽化された型がIEnumerableだったこと
  • IEnumerableを利用するコードが複数あったため
  • IEnumerableは「遅延実行」をサポートしているため

特に三つ目の意味を理解するためには、以下のサイトが参考になりました。

https://pleiades.io/help/resharper/PossibleMultipleEnumeration.html

この記事で紹介されているコード

IEnumerable<string> names = GetNames();
foreach (var name in names)
    Console.WriteLine("Found " + name);

var allNames = new StringBuilder();
foreach (var name in names)
    allNames.Append(name + " ");

一行目のGetNames()を実行することで、即座に値が返されてnames変数に値が代入されているように見えるのですが、 実はGetNames()が実行されるのは、2行目以降になる場合があるそうです。これはIEnumerableの「遅延実行」の仕組みが起因しています。

さらに、上記のコードのようにnamesが複数回呼び出されると、 GetNames()をその回数分だけ実行する可能性があります。 例えば、データベースに変更があると、一回目と二回目のnamesで取得する値が変わってしまうとのことです。

これは気づかないうちに、リスト内に矛盾を引き起こす及び、深刻なパフォーマンス問題を作るかもしれませんね。

解決策

IEnumerableで受け取るのではなく、ToList()やToArray()で「即時実行」させてしまいましょう。

// これで実体がitemsに代入されて、getItemQuery()は一回のみの実行となる!
// つまり、itemsがキャッシュのような役割をもつことになる!
var items = GetItemQuery().ToList();

if(item.Count == 0)
// DoSomething

items.DoSomething

ただし、遅延実行をすることで、無限ストリームを作成できたりするメリットもあるので、用法は正しく理解するようにしましょう🙏

まとめ

C#は遅延実行などのモダンな機能が導入されるのが早くていいですね。 Javaでかゆいところに手が届かなかったものが、C#ではほとんど解決されていて最高という気分です。