LINQと同じ動作をするメソッドを実装することにより、具体的なコードを使って理解する。 またコードレベルでの動作を知ることにより適切な形でLINQを使うことができるようにする。

以下のサンプルでは簡単化のため引数チェックなどの処理は省略している。 また、ここではLINQのメソッド構文のみを扱い、クエリ構文は扱わない。

導入

Count

LINQのメソッドがどのような実装になっているかを理解するために、Countメソッドと同じ動作をするメソッドを実装することを考える。 まず、LINQのCountメソッドを使って要素数を取得する場合は次のようになる。

Countメソッド
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(source.Count());
  }
}

これと同等の処理を行う方法のひとつとして次のようにする方法が考えられる。 つまり、単にforeachによりソースシーケンス(リスト・配列・コレクションや、LINQメソッドの戻り値として返されるIEnumerable<T>など)を列挙し、列挙できた要素を数え上げる。 (ICollection<T>ならばCountプロパティを参照することもできるが、ここではIEnumerable<T>に限定して考える)

IEnumerable<T>の要素数を数える方法
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    int count = 0;

    foreach (var e in source) {
      count++; // 列挙した回数=列挙できた要素数を計上する
    }

    Console.WriteLine(count);
  }
}

次に、このアルゴリズムを汎用的に使用できるようメソッド化すると次のようになる。

メソッド化した計上アルゴリズム
using System;
using System.Collections.Generic;

class Sample {
  static int Count(IEnumerable<int> source)
  {
    int count = 0;

    foreach (var e in source) {
      count++;
    }

    return count;
  }

  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(Count(source));
  }
}

これでは対象がint型のソースシーケンス(IEnumerable<int>)に限定されてしまうので、さらに汎用的に使用できるよう型指定をパラメータ化してジェネリックメソッドにし、任意の型のソースシーケンス(IEnumerable<TSource>)に対して使用できるようにする。

ジェネリックメソッド化した計上アルゴリズム
using System;
using System.Collections.Generic;

class Sample {
  static int Count<TSource>(IEnumerable<TSource> source)
  {
    int count = 0;

    foreach (var e in source) {
      count++;
    }

    return count;
  }

  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(Count(source));
  }
}

さらに、LINQのCountメソッドと同様インスタンスメソッドのように呼び出せるようにするため、このメソッドを拡張メソッドにする。 (詳細:拡張メソッド) 拡張メソッドは静的クラス(static class, VBではモジュール)で実装する必要があるので、先のメソッドをクラス化する。 (ここではEnumerableというクラス名にする)

拡張メソッド化した計上アルゴリズム
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static int Count<TSource>(this IEnumerable<TSource> source)
  {
    int count = 0;

    foreach (var e in source) {
      count++;
    }

    return count;
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(source.Count()); // インスタンスメソッドのように呼び出せる
  }
}

これでLINQのCountメソッドと同等のメソッドを作ることができた。 ここまでの要点は、

  1. LINQのメソッドは任意の型のソースシーケンス(IEnumerable<TSource>)を対象としたアルゴリズム
  2. アルゴリズムを拡張メソッドとして実装することでインスタンスメソッドのように呼び出せる

First, Last

Firstメソッドも同様にして実装することができる。 Firstメソッドはソースシーケンスから最初の要素を取得して返す。 ソースシーケンスが空の場合は最初の要素を定義できないため、例外をスローする。

Firstメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static TSource First<TSource>(this IEnumerable<TSource> source)
  {
    foreach (var e in source) {
      return e; // 最初に列挙された要素を返す
    }

    // 列挙する要素がない=シーケンスが空の場合は例外をスローする
    throw new InvalidOperationException("ソースシーケンスが空です");
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(source.First());
  }
}
実行結果
0

一方FirstOrDefaultメソッドでは、ソースシーケンスが空の場合にはデフォルトの値を返すとされている。 これを実装すると次のようになる。

FirstOrDefaultメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source)
  {
    foreach (var e in source) {
      return e; // 最初に列挙された要素を返す
    }

    // 列挙する要素がない=シーケンスが空の場合はデフォルトの値を返す
    return default(TSource);
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {2, 3, 4};

    Console.WriteLine(source.FirstOrDefault());

    IEnumerable<int> empty = new List<int>();

    Console.WriteLine(empty.FirstOrDefault());
  }
}
実行結果
2
0

Firstメソッドでは例外をスローしていた箇所を、FirstOrDefaultメソッドではデフォルト値を返すように置き換えている。 ジェネリックメソッドで型に応じたデフォルト値を返すにはdefault()(VBではNothing)を使用する。 (型の種類・サイズ・精度・値域 §.型のデフォルト値)

LastメソッドおよびLastOrDefaultメソッドも、default(またはNothing)を使うことで同様に実装できる。

Lastメソッド・LastOrDefaultメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static TSource Last<TSource>(this IEnumerable<TSource> source)
  {
    TSource last = default(TSource);
    bool isEmpty = true;

    foreach (var e in source) {
      last = e; // 最後に列挙された要素を保持する
      isEmpty = false; // ひとつでも列挙されたらfalse(=空ではない)にする
    }

    if (isEmpty)
      // 要素が1つも列挙されなかった場合
      throw new InvalidOperationException("ソースシーケンスが空です");
    else
      // 列挙された場合は最後に列挙された要素を返す
      return last;
  }

  public static TSource LastOrDefault<TSource>(this IEnumerable<TSource> source)
  {
    TSource last = default(TSource);

    foreach (var e in source) {
      last = e; // 最後に列挙された要素を保持する
    }

    // 最後に列挙された要素、またはデフォルトの値を返す
    return last;
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(source.Last());

    IEnumerable<int> empty = new List<int>();

    Console.WriteLine(empty.LastOrDefault());
  }
}

Skip, Take

Skipメソッドはソースシーケンスから最初のn個の要素を除いた部分(スキップした部分)を取り出す。 逆にTakeメソッドはソースシーケンスから最初のn個の要素のみを取り出す。

Skipメソッド・Takeメソッド
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    var source = new List<int>() {0, 1, 2, 3, 4};

    // 先頭から3つの要素を除いた残りの要素を取り出して表示する
    Console.WriteLine(string.Join(", ", source.Skip(3)));

    // 先頭から3つの要素を取り出して表示する
    Console.WriteLine(string.Join(", ", source.Take(3)));
  }
}
実行結果
3, 4
0, 1, 2

これを実装すると次のようになる。

Skipメソッド・Takeメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> source, int count)
  {
    var result = new List<TSource>(); // 結果を格納するリスト

    foreach (var e in source) {
      if (0 < count)
        count--;
      else
        result.Add(e); // 指定された要素数を列挙した時点から追加する
    }

    return result;
  }

  public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
  {
    var result = new List<TSource>(); // 結果を格納するリスト

    foreach (var e in source) {
      if (0 < count)
        result.Add(e); // 指定された要素数となるまで追加する
      else
        break; // 指定された要素数を超えたら列挙を中断してその時点までの結果を返す

      count--;
    }

    return result;
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(string.Join(", ", source.Take(3)));

    Console.WriteLine(string.Join(", ", source.Skip(3)));
  }
}

ここで、Skipメソッド・Takeメソッドのドキュメントでは以下のように記述されている。

このメソッドは遅延実行を使用して実装されます。アクションの実行に必要なすべての情報を格納するオブジェクトがすぐに返されます。このメソッドによって表されるクエリは、オブジェクトの GetEnumerator メソッドを直接呼び出すか、foreach (Visual C# の場合) または For Each (Visual Basic の場合) を使用することによってオブジェクトが列挙されるまで実行されません。

Enumerable.Take<TSource> メソッド (IEnumerable<TSource>, Int32)

これはSkipメソッド・Takeメソッドに限らずIEnumerable<T>を返すLINQメソッドに共通するものとなっている。

IEnumerable<T>の列挙結果をyieldを使って生成するメソッドでは遅延実行が行われる。 先の実装では結果をList<TSource>に格納して返したが、かわりにyieldによって逐次結果を返すようにする。 (VBではさらにメソッドに修飾子Iteratorを付ける)

yieldを使って遅延実行するようにする
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> source, int count)
  {
    foreach (var e in source) {
      if (0 < count)
        count--;
      else
        yield return e; // yieldによって結果を返す
    }
  }

  public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
  {
    foreach (var e in source) {
      if (0 < count)
        yield return e; // yieldによって結果を返す
      else
        break;

      count--;
    }
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(string.Join(", ", source.Skip(3)));

    Console.WriteLine(string.Join(", ", source.Take(3)));
  }
}

これにより動作は変わらず、遅延実行するようになる。

実行結果
3, 4
0, 1, 2

ここまでの要点は

  1. Skip()などのソースシーケンスに加工して新たなシーケンス(IEnumerable<T>)を生み出すLINQメソッドでは、遅延実行が行われる
  2. 遅延実行yieldによって実装することができる

述語(predicate)を引数にとるメソッド

Where

Whereメソッドは指定された条件(あるいは述語、predicate)に基づいてシーケンス内の要素をフィルタして新たなシーケンスを生成することができる。 例えば偶数だけをフィルタするには次のようにする。

Whereメソッドを使ったシーケンスのフィルタ
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(string.Join(", ", source.Where(val => val % 2 == 0))); // 偶数(=2で割った余りが0)の要素だけをフィルタする
  }
}
実行結果
0, 2, 4

フィルタの条件は、TSource型の要素を検査してboolを返すメソッドのデリゲート(Func<TSource, bool>)を引数predicateに渡すことによって指定する。 ラムダ式を使わずに条件を記述するとこのようになる。 ここでは要素の型TSourceintなので、predicateにはデリゲートFunc<int, bool>を渡す。

フィルタの条件を定義したメソッド
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  // 値が偶数か否かを判定するメソッド
  static bool IsEven(int val)
  {
    if (val % 2 == 0)
      return true;
    else
      return false;
  }
  
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    var predicate = (Func<int, bool>)IsEven; // フィルタの条件

    Console.WriteLine(string.Join(", ", source.Where(predicate)));
  }
}

これをLINQを使わずに記述すると次のようになる。

LINQを使わずにソースシーケンスをフィルタする
using System;
using System.Collections.Generic;

class Sample {
  // 値が偶数か否かを判定するメソッド
  static bool IsEven(int val)
  {
    if (val % 2 == 0)
      return true;
    else
      return false;
  }
  
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    var predicate = (Func<int, bool>)IsEven; // フィルタの条件

    var result = new List<int>(); // フィルタした結果を代入するリスト

    foreach (var e in source) {
      if (predicate(e))
        result.Add(e); // 条件に一致する要素のみをリストに格納する
    }

    Console.WriteLine(string.Join(", ", result));
  }
}

これまでの例と同様、このアルゴリズムを遅延実行するようにし、かつ拡張メソッドとして呼び出せるようにするとこのようになる。

Whereメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
  {
    foreach (var e in source) {
      if (predicate(e))
        yield return e; // 条件に一致する要素のみをyieldによって返す
    }
  }
}

class Sample {
  // 値が偶数か否かを判定するメソッド
  static bool IsEven(int val)
  {
    if (val % 2 == 0)
      return true;
    else
      return false;
  }
  
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(string.Join(", ", source.Where(IsEven)));

    // 当然、ラムダ式を使って呼び出すこともできる
    Console.WriteLine(string.Join(", ", source.Where(val => val % 2 == 0)));
  }
}

All, Any

Allメソッドはソースシーケンス内のすべての要素が条件を満たすかどうかを調べる。 またAnyメソッドは条件を満たす要素が一つでもあるかどうかを調べる。

Allメソッド・Anyメソッド
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  // 値が偶数か否かを判定するメソッド
  static bool IsEven(int val)
  {
    if (val % 2 == 0)
      return true;
    else
      return false;
  }
  
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(source.All(IsEven)); // すべての要素が偶数かどうか
    Console.WriteLine(source.Any(IsEven)); // 偶数の要素を含むかどうか
  }
}
実行結果
False
True

これもWhereメソッドの場合と同様にFunc<TSource, bool>を使って次のように実装することができる。

Allメソッド・Anyメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static bool All<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
  {
    foreach (var e in source) {
      if (!predicate(e))
        return false; // 条件に一致しない要素があった時点でfalseを返す
    }

    return true; // すべて条件に一致する場合はtrueを返す
  }

  public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
  {
    foreach (var e in source) {
      if (predicate(e))
        return true; // 条件に一致する要素があった場合はtrueを返す
    }

    return false; // なければfalseを返す
  }
}

class Sample {
  // 値が偶数か否かを判定するメソッド
  static bool IsEven(int val)
  {
    if (val % 2 == 0)
      return true;
    else
      return false;
  }
  
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(source.All(IsEven)); // すべての要素が偶数かどうか
    Console.WriteLine(source.Any(IsEven)); // 偶数の要素を含むかどうか
  }
}

SkipWhile, TakeWhile

要素数を指定するSkipメソッド・Takeメソッドとは異なり、SkipWhileメソッドTakeWhileメソッドでは要素を選別する条件をFunc<TSource, bool>で指定することができる。

SkipWhileメソッド・TakeWhileメソッド
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(string.Join(", ", source.SkipWhile(val => val <= 2))); // 要素が2以下である間はスキップする
    Console.WriteLine(string.Join(", ", source.TakeWhile(val => val <= 2))); // 要素が2以下である間は要素を抽出する
  }
}
実行結果
3, 4
0, 1, 2

これもAllメソッド・Anyメソッドと同様に次のように実装できる。

SkipWhileメソッド・TakeWhileメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static IEnumerable<TSource> TakeWhile<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
  {
    foreach (var e in source) {
      if (predicate(e))
        yield return e; // 要素が条件に一致する場合はその要素を返す
      else
        break; // 一致しなくなった時点で列挙を中断する
    }
  }

  public static IEnumerable<TSource> SkipWhile<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
  {
    bool skip = true;

    foreach (var e in source) {
      if (skip) {
        if (!predicate(e)) {
          yield return e; // 要素が条件に一致しなくなった時点でその要素を返す
          skip = false; // 以降の要素をすべて返す
        }
      }
      else {
        yield return e;
      }
    }
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(string.Join(", ", source.SkipWhile(val => val <= 2))); // 要素が2以下である間はスキップする
    Console.WriteLine(string.Join(", ", source.TakeWhile(val => val <= 2))); // 要素が2以下である間は要素を抽出する
  }
}
実行結果
3, 4
0, 1, 2

変換関数(selector)を引数にとるメソッド

Select

Selectメソッドは指定された変換関数(Func<TSource, TResult>)に基づいてシーケンス内の全要素を変換し、新しいシーケンスを返す。 変換関数(Convert<TInput, TOutput>)を使って配列の型を変換するArray.ConvertAllメソッドと似ている。 (配列操作 §.全要素の変換 (ConvertAll))

次の例では、クラスから特定のフィールドを選択する変換関数を指定することにより、クラス型のシーケンスから各フィールドの値を抽出したシーケンスを生成している。

Selectメソッド
using System;
using System.Collections.Generic;
using System.Linq;

class Account {
  public int ID;
  public string Name;

  public override string ToString()
  {
    return string.Format("{0}:{1}", ID, Name);
  }
}

class Sample {
  static void Main()
  {
    IEnumerable<Account> source = new List<Account>() {
      new Account() {ID = 0, Name = "Alice"},
      new Account() {ID = 1, Name = "Bob"},
      new Account() {ID = 2, Name = "Charlie"},
    };

    Console.WriteLine(string.Join(", ", source));

    // シーケンス内の各要素からNameプロパティの値だけを'選択'する
    var names = source.Select(account => account.Name);

    Console.WriteLine(string.Join(", ", names));

    // シーケンス内の各要素からIDプロパティの値だけを'選択'する
    var ids = source.Select(account => account.ID);

    Console.WriteLine(string.Join(", ", ids));
  }
}
実行結果
0:Alice, 1:Bob, 2:Charlie
Alice, Bob, Charlie
0, 1, 2

もうひとつ別の例として、シーケンス内の全要素の平方根を求めるには次のようにする。

Selectメソッドを使ってシーケンスの平方根を求める例
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    Console.WriteLine(string.Join(", ", source.Select(val => Math.Sqrt(val))));
  }
}
実行結果
0, 1, 1.4142135623731, 1.73205080756888, 2

Selectメソッドでは、変換関数(Func<TSource, TResult>)を引数selectorで指定する。 変換関数の引数の型は元のシーケンスと同じ型(TSource)で、戻り値は変換後の型(TResult)となる。 ラムダ式を使わずに変換関数を記述するとこのようになる。

ラムダ式を使わずに変換関数を記述した例
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  // 与えられた値をその平方根に変換するメソッド
  static double Sqrt(int val)
  {
    return Math.Sqrt(val);
  }

  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    var selector = (Func<int, double>)Sqrt; // intの値をdoubleに変換する変換関数

    Console.WriteLine(string.Join(", ", source.Select(selector)));
  }
}

これをLINQを使わずに記述すると次のようになる。

LINQを使わずにソースシーケンスを変換する例
using System;
using System.Collections.Generic;

class Sample {
  // 与えられた値をその平方根に変換するメソッド
  static double Sqrt(int val)
  {
    return Math.Sqrt(val);
  }

  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    var selector = (Func<int, double>)Sqrt; // 変換関数

    var result = new List<double>(); // 変換した結果を代入するリスト

    foreach (var e in source) {
      result.Add(selector(e)); // 列挙された要素に変換関数を適用してリストに追加する
    }

    Console.WriteLine(string.Join(", ", result));
  }
}

これまでの例と同様、このアルゴリズムを遅延実行するようにし、かつ拡張メソッドとして呼び出せるようにするとこのようになる。

Selectメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
  {
    foreach (var e in source) {
      yield return selector(e); // 変換関数を適用した結果をyieldによって返す
    }
  }
}

class Sample {
  // 与えられた値をその平方根に変換するメソッド
  static double Sqrt(int val)
  {
    return Math.Sqrt(val);
  }

  static void Main()
  {
    IEnumerable<int> source = new List<int>() {0, 1, 2, 3, 4};

    var selector = (Func<int, double>)Sqrt; // 変換関数

    Console.WriteLine(string.Join(", ", source.Select(selector)));

    // 当然、ラムダ式を使って呼び出すこともできる
    Console.WriteLine(string.Join(", ", source.Select(val => Math.Sqrt(val))));
  }
}

Average, Sum

AverageメソッドSumメソッドは変換関数によって平均・合計として計上する値を各要素から選択する。 これによりシーケンス内の全要素から特定の値に基づいた平均・合計を求めることができる。

Averageメソッド・Sumメソッド
using System;
using System.Collections.Generic;
using System.Linq;

class File {
  public string Name;
  public int Length;
}

class Sample {
  static void Main()
  {
    IEnumerable<File> source = new List<File>() {
      new File() {Name = "sample.txt", Length = 123},
      new File() {Name = "sample.jpg", Length = 45679},
      new File() {Name = "sample.zip", Length = 3145},
    };

    Console.WriteLine("Average = {0}", source.Average(file => file.Length)); // Lengthの平均を求める

    Console.WriteLine("Sum = {0}", source.Sum(file => file.Length)); // Lengthの合計を求める
  }
}
実行結果
Average = 16315.6666666667
Sum = 48947

これをLINQを使わずに記述すると次のようになる。

LINQを使わずに平均・合計を求める例
using System;
using System.Collections.Generic;

class File {
  public string Name;
  public int Length;
}

class Sample {
  // FileクラスのLengthフィールドの値を参照・取得するメソッド
  static int GetLength(File file)
  {
    return file.Length;
  }

  static void Main()
  {
    IEnumerable<File> source = new List<File>() {
      new File() {Name = "sample.txt", Length = 123},
      new File() {Name = "sample.jpg", Length = 45679},
      new File() {Name = "sample.zip", Length = 3145},
    };

    var selector = (Func<File, int>)GetLength; // 合計を求めるための値を選択するための変換関数

    int sum = 0;
    int count = 0;

    foreach (var e in source) {
      sum += selector(e); // 要素から合計として加算する値を取得する
      count++;
    }

    Console.WriteLine("Average = {0}", sum / (double)count);

    Console.WriteLine("Sum = {0}", sum);
  }
}

これまでの例と同様、このアルゴリズムを遅延実行するようにし、かつ拡張メソッドとして呼び出せるようにするとこのようになる。

Averageメソッド・Sumメソッドの実装
using System;
using System.Collections.Generic;

public static class Enumerable {
  public static double Average<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
  {
    int sum = 0;
    int count = 0;

    foreach (var e in source) {
      sum += selector(e);
      count++;
    }

    if (count == 0)
      throw new InvalidOperationException("ソースシーケンスが空です");
    else
      return sum / (double)count;
  }

  public static int Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
  {
    int sum = 0;

    foreach (var e in source) {
      sum += selector(e);
    }

    return sum;
  }
}

class File {
  public string Name;
  public int Length;
}

class Sample {
  // FileクラスのLengthフィールドの値を参照・取得するメソッド
  static int GetLength(File file)
  {
    return file.Length;
  }

  static void Main()
  {
    IEnumerable<File> source = new List<File>() {
      new File() {Name = "sample.txt", Length = 123},
      new File() {Name = "sample.jpg", Length = 45679},
      new File() {Name = "sample.zip", Length = 3145},
    };

    var selector = (Func<File, int>)GetLength; // 合計を求めるための値を選択するための変換関数

    Console.WriteLine("Average = {0}", source.Average(selector)); // Lengthの平均を求める

    Console.WriteLine("Sum = {0}", source.Sum(selector)); // Lengthの合計を求める

    // 当然、ラムダ式を使って呼び出すこともできる
    Console.WriteLine("Average = {0}", source.Average(file => file.Length));

    Console.WriteLine("Sum = {0}", source.Sum(file => file.Length));
  }
}