Linqのイメージ

Linqでは要素を取得する処理は本当にその要素が必要なってはじめて実行されます。これを遅延実行と呼びます。Linqによる遅延実行は、複数のデバイスが通信によってデータを要求・応答している様子をイメージをすればよいと思います。

Linqと遅延実行のイメージ

Linqのイメージ

デバイス1がデータを発生します。デバイス2がデータを中間処理します。デバイス3がデータを最終処理します。処理はデバイス3がデバイス2に対してデータを要求することで開始します。デバイス2は要求を受けてデバイス1に対してデータを要求します。デバイス1はデバイス2に対して応答を返します。デバイス2は応答を受けたらデバイス3に応答を返します。これが繰り返されるのです。

デバイス1やデバイス2は、図中自分より右のデバイスにデータを要求されて初めて応答します。遅延実行はこのような仕組みで働きます。

遅延実行で省メモリ

リスト1

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LinqLazy
{
  static class EnumerableExtentions
  {
    public static void Each<T>(this IEnumerable<T> source, Action<T> func)
    {
      foreach (var elm in source) {
        func(elm);
      }
    }
  }

  class Program
  {
    
    public static IEnumerable<int> Generator()
    {
      for(var i=0; i<10000; i++){
        yield return i;
      }
    }

    static void Main(string[] args)
    {
      Generator().Where(x => x > 2).Each(i => Console.WriteLine(i));
      Console.ReadKey();
    }
  }
}
Linqシーケンス

リスト1の例はGeneratorが0から10000までの整数を発生させます。WhereGeneratorから渡された要素が2より大きいものだけを通すようにフィルタリングします。Each内で結果を表示します。遅延実行の様子はシーケンス図で確認してください。MoveNext Currentがデータの要求、yield returnが応答です。

もしGeneratorのすべての処理が終わってからWhereの処理に移るならば、int約10,000個分のバッファが必要になります。しかし遅延実行を行っているために1個分のバッファで済むのです。この場合遅延実行のメリットは省メモリです。

遅延実行で無限Generator

リスト2

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LinqLazy
{
  static class EnumerableExtentions
  {
    public static void Each<T>(this IEnumerable<T> source, Action<T> func)
    {
      foreach (var elm in source) {
        func(elm);
      }
    }
  }

  class Program
  {
    public static IEnumerable<int> Generator()
    {
      //無限に整数をカウントアップする
      for(var i=0; ; i++){
        yield return i; 
      }
    }

    static void Main(string[] args)
    {
      Generator().TakeWhile(x => x < 3).Each(i => Console.WriteLine(i));
      Console.ReadKey();
    }
  }
}
}

リスト2ではGeneratorが無限に整数を発生させています。TakeWhileGeneratorから渡された要素が3より小さい間だけ通すようにフィルタリングします。無限ループにならずに終了するのは0,1,2,3だけが処理されるからです。無限ループにならずに実行できるのは遅延実行のおかげです。

TakeWhileWhereに変えると遅延実行はされるものの処理は続き無限ループになります。無限に要素を返すジェネレータはTakeWhileのように処理を打ち切るフィルタと組み合わせて使う必要があります。

遅延実行されない場合

Linqがいつも遅延実行されるわけではありません。Max,Min,OrderByなどは原理的にすべての要素が揃わないと最初の要素すら決まりませんのでこのようなクエリ演算子は遅延実行されません(マイクロソフトでは集中評価と呼んでいるようです)。

Linqで言う遅延実行はメソッド呼び出し時の引数の遅延評価とは意味が異なります。Linqに限らず引数にデリゲート・ラムダ式を指定して評価を呼び出し後に行うことを遅延評価と呼んでいます。遅延実行のためには遅延評価も必要です。