LINQのキホン(.NET)


.NET の LINQ

いままで適当にしか .NET や C# に取り組んでいなかったですが、いまの職場はC#/.NETのプロジェクトが多いので、色々と見直しておこうと思い、まずは LINQ の自分用メモです。

  • LINQ (クエリ式)
    SQL文に似ている宣言クエリ構文を用いた記述方法。LINQ to Objectsの方がやれることが多いので読めるようにはなっておいた方が良いが、積極的に使う必要はないかと。

  • LINQ to Objects
    任意のコレクションを照会するための方法。foreachでごりごりとした実装から開放され、宣言的なコードによって、強力なフィルター処理、並べ替え、およびグループ化機能を最小限のアプリケーション コードで実現できます。

  • Parallel LINQ (PLINQ)
    LINQ to Objectsを並列実行するためのAPI。クエリの処理速度が大幅に向上します。

  • LINQプロバイダー
    直接LINQ to Objectsを使わずに、データソースに特化したプロバイダーがあります。

    • LINQ to XML (XLinq)
    • LINQ to DataSet
    • LINQ to SQL
    • LINQ to Entities

LINQプロバイダーは自作することが出来ますので、他にもサードパーティから LINQ to LDAP、LINQ To Oracle、LINQ to CSV、LINQ to Wiki、LINQ to JSON、LINQ to EXCEL、LINQ to twitter などが存在しますし、なければ作るだ。

LINQ to Objects のキホン

キホンというのは使い方ではなくて、仕組みです。コレクションに対してwhereやらselectやらsumやらを使って処理するのは見れば分かります。表面的な使い方だけを知っても、パフォーマンスが悪い、動きが意図しないという事態に対して対処できませんから。

LINQ to Objects の構成

LINQ to Objects は、IEnumerableインターフェース、IEnumeratorインターフェース、Enumerableクラスの拡張メソッドという3つの構成になっています。

  • LINQ で反復処理するためには IEnumerableインターフェースを持っていなければなりません。.NETのコレクションは全て IEnumerableインターフェースを持つようになっています。
  • IEnumerableインターフェースは GetEnumerator()しか持っていません。反復処理をするためには IEnumeratorインターフェースを使います。
  • IEnumeratorインターフェースも Currentプロパティ、Dispose()、MoveNext()、Reset() しか持っていません。
  • 集計や比較といったコレクションに対する操作はすべてEnumerableクラスの拡張メソッドにあります。

拡張メソッドは、既存クラスにメソッドを追加する仕組みで、既存クラスにはいっさいの変更を加えません。これいいですよね。自分の作るアーキテクチャーの参考になります。

LINQ to Objects の特徴

LINQ to Objects は以下の特徴を備えています。これらによって効率よく高速に動作し、メモリも消費しないという作りになっています。(この内容は「基礎からわかるLINQマジック」を参考というか、ほぼそのまま)

  • LINQは実行時に分解・再構築されてから実行される
  • LINQは無駄にメモリを消費しない
  • LINQ拡張メソッドの処理は遅延実行される

LINQは実行時に分解・再構築されてから実行される

以下のように複数行に分かれていても、実行時に分解・再構築して一つのループにしてくれます。コードの可読性を重視できるので良いですね。

IEnumerable<int> numbers = Enumerable.Range(1,10);
var evens = numbers.Where(n=>n%2==0); //一つ目のループ
var sum = evens.Sum(); //二つ目のループ・・・のようには別れません

LINQは無駄にメモリを消費しない

LINQは中間結果を持つための別途コレクションを作ることはありません。MoveNext()を持つIEnumeratorが繰り返し実行されるわけですが、Currentプロパティしか更新しないように作られています。

LINQ拡張メソッドの処理は遅延実行される

LINQは分解・再構築されてから実行されるので、コードが記述してある箇所で実行されるわけではありません。すなわち遅延実行されているということです。 ToList()、Average、Count、First、Max といった非IEnumerableを返す場合に連鎖的にすべての処理が実行されます。

LINQ拡張メソッドの作り方

標準で様々な拡張メソッドがあります。
MSDNのIEnumerable インターフェイスを見ると載っています。
https://msdn.microsoft.com/ja-jp/library/9eekhta0.aspx

しかし、いざ実装すると要件に対してもっと便利なフィルターが欲しくなる時があります。そんな時は自分で拡張メソッドを作ることが出来ます。拡張メソッドはLINQのメリットを損なわないように次の特徴を持たせるように実装します。

  • 入力は IEnumerable もしくは IQueryable であること
  • 戻り値は IEnumerable、IQueryable またはスカラーである
  • IEnumerable、IQueryable を返すときは遅延実行できること
public static IEnumerable<T> LessThan<T>(this IEnumerable<T> numbers, T threshold) where T : IComparable
{
  foreach (var n in numbers)
  {
    if(n.CompareTo(threshold) < 0)
      yield return n;
  }
}

yield (イールド) というのは、メソッド、演算子、または get アクセサーが反復子であることを示すことになります。これを使うとカスタム コレクション型の IEnumerable および IEnumerator パターンを実装するときに明示的な余分なクラスが不要になります。遅延評価の行われる反復処理を容易に実装することができます。

LINQデータソースの作り方

IEnumerable を受けつけるようにすれば拡張メソッドですが、IEnumerable を持つようにすればデータソースになれます。

データソース
punlic class MyData
{
  public string Kind { get; set; }
  public int Value { get; set; }
}

public class MyDataDataSource : IEnumerable<MyData>
{
  public IEnumerator<MyData> GetEnumerator()
  {
    return GetCsvEnumerator();
  }
  IEnumerator IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }

  private MyDataDataSource ()
  {
  }

  private string filePath;
  public static MyDataDataSource Read(string path)
  {
    return new MyDataDataSource () { filePath = path };
  }

  private IEnumerator<MyData> GetCsvEnumerator()
  {
    foreach(var line in File.ReadLines(filePath))
    {
      string[] data = line.Split(',');
      string kind = data[0].Trim();
      int value = 0;
      int.TryParse(data[1].Trim(), value);
      yield return new MyData(){ Kind = kind, Value = value);
    }
  }
}
利用側
  var myDatas = MyDataDataSource.Read("xxx.csv");
  var over100 = myDatas.Where(s => { return s.Value > 100;});

yield のおかげでこちらもすっきりとした実装で済みます。
要件によって特殊なデータソースがある場合、標準のLINQのままがんばるより、LINQデータソースを作ってしまった方がスッキリするでしょう。

LINQプロバイダーの作り方

LINQプロバイダーは IQueryable と IQueryProviderインターフェイスで構成されます。

  • IQueryable は式木を構築するためのインターフェース
  • Queryable 拡張メソッドは式木を簡単に構築するためのユーティリティ

で、ちょっと目の前に作るものがないので、具体的なコードは割愛します。というのも、LINQプロバイダーはデータベースやネットワークから条件を絞り込みながら取得したい場合に作成するもので、簡単な要件で作るものではないからです。

MSDNでは、米国の航空画像のデータベースからMCFを用いてアクセスするサンプルが掲載されています。
https://msdn.microsoft.com/ja-jp/library/bb546158(v=vs.120).aspx

また「LINQ IQueryable Toolkit」というのが公開されています。少ない実装でLINQプロバイダーを作ることが出来るらしいので、機会を見つけて使ってみようと思います。
https://blogs.msdn.microsoft.com/mattwar/2008/11/18/linq-building-an-iqueryable-provider-series/
https://github.com/mattwar/iqtoolkit

なんだかんだと .NET の LINQ は良く出来てます

Javaでもラムダ式と StreamAPI があるわけですが、機能差が歴然としています。StreamAPI が未だ途上なせいもありますが、.NET の LINQ をJavaに移植するプロジェクトもあるくらいなので、Microsoftの作り込みは素晴らしいと言えます。別に肩持つわけじゃないし、アンチな時もありますけどね。