Visual Studio 2008から導入された標準クエリ演算子を利用するスタイルでは、IEnumerable<T>を返すメソッドから始まり、中間ではIEnumerable<T>を受け取ってIEnumerable<T>を返すメソッド、最後はIEnumerable<T>を受け取って最終処理するメソッドを数珠(じゅず)繋ぎにします。IEnumerable<T>を受け取るとは引数で受け取ることを考えてしまいますが、そうではなくIEnumerable<T>のメソッドとして実装することでフィルタ処理を左から右に順にドットで結びつけて表現できるので、処理順序とプログラム記述順序が一致します。

各フィルタ内部ではyieldを使ってコンテキストスイッチを行い次のフィルタにデータを渡す処理を内部で繰り返しています。そのためフィルタを記述するのに繰り返しをその都度記述する必要はありません。

IEnumerableによるフィルタ

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

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

    public static IEnumerable<T> Map<T>(this IEnumerable<T> source, Func<T, T> func)
    {
      foreach (var elm in source) {
        yield return func(elm);
      }
      yield break;
    }
  }

  class Program
  {
    public static IEnumerable<string> FileReader()
    {
      string line;
      while ((line = Console.ReadLine()) != null) {
        yield return line;
      }
      yield break;
    }

    static void Main(string[] args)
    {
      FileReader().Map(line => line.ToUpper()).Each(line => Console.WriteLine(line));
    }
  }
}

NetFrameworkではSelect,Whereなどのメソッドが用意されていますがこれらはどのように作ればよいのでしょう。IEnumerable<T>を利用するスタイルで使えるEachMapというメソッドを作ってみます。EachIEnumerable<T>を受け取って、引数で指定したメソッドに各要素を順に渡すメソッド、Mapは、指定したメソッドに各要素を順に渡すだけでなくそのメソッドが何らかの変換を行って値を返します。IEnumerable<T>はNetframeworkに用意されている型ですが、作り付けの型に新たなメソッドを追加しなければならないことになります。また、IEnumerable<T>はクラスではなくインタフェースですから通常は実装を持たないはずです。なおさらメソッドを実装することが難しく思えます。2008からは拡張メソッドによってそれが可能になっています。

Each,Mapは共に拡張メソッドとして実装しました。Eachの第2引数funcは各要素を受け取って処理するメソッドを指定します。Action<T>は引数を一つ受け取り、値を返さないメソッドを表します。一方Mapでは値を返しますから、Func<T, T>となっています。Func<T1, T2>とは一つの引数T1を受け取り、T2を返すメソッドを表します。ここでは両方Tですから引数と同じ型の値を返すのです。

使用例は、標準入力を順に読み取り、大文字にして標準出力に書き込むコンソールプログラムです。英文を入力してEnterを押せば次の行に大文字に変換された文字列が表示されます。終了させるにはcontrol-Zに続いてEnterを押します。

FileReaderは標準入力を1行ずつ読み取るメソッドです。FileReaderは次のMapに結果を渡します。Mapの引数には大文字変換を行うラムダ式が指定されています。MapはさらにEachに結果を渡します。Eachの引数には渡された文字列を表示するラムダ式が指定されています。

これを発展させれば多段階の処理が必要なフィルタを実現することもできます。

この例では頻繁にyieldが呼ばれます。yieldのパフォーマンスは下記ページを参考にしてください。

yieldのパフォーマンス(C#)