LINQ: .NET 統合言語クエリ
Don Box, Anders Hejlsberg
January 2007
日本語版最終更新日 2007 年 7 月 17 日
適用対象 :
Visual Studio 2008
.Net Framework 3.5
概要 : この資料では、.NET Framework に追加された汎用クエリ機能について説明します。この汎用クエリ機能は、リレーショナル データや XML データだけでなく、あらゆる情報ソースに適用されます。この機能を .NET LINQ (Language Integrated Query : 統合言語クエリ) と呼びます。
目次
.NET LINQ
標準クエリ演算子の概要
LINQ プロジェクトをサポートする言語機能
標準クエリ演算子の詳細
クエリ構文
LINQ to SQL: SQL 統合
LINQ to XML: XML 統合
まとめ
.NET LINQ
オブジェクト指向 (OO) プログラミング テクノロジの進化は、20 年の歳月を経て、ようやく落ち着きを見せています。プログラマも、クラス、オブジェクト、メソッドのような機能を使いこなせるようになりました。こうしたテクノロジの現状や次世代のテクノロジに目を向けると、OO テクノロジを使用してネイティブに定義されていない情報へのアクセスや統合の複雑さを軽減することが次の大きな課題であることが明らかになってきています。OO テクノロジに対応しない情報の中で最も一般的な情報ソースが、リレーショナル データベースと XML の 2 つです。
.NET Framework では、プログラミング言語やランタイムにリレーショナル固有の機能や XML 固有の機能を追加するのではなく、LINQ プロジェクトという汎用性の高いアプローチを採用しました。これにより .NET Framework に汎用クエリ機能が追加され、リレーショナル データや XML データだけでなく、あらゆる情報ソースにクエリ機能が適用されます。この機能を .NET LINQ (Language Integrated Query : 統合言語クエリ) と呼びます。
"統合言語クエリ (LINQ : Language Integrated Query)" という用語が示すとおり、開発者が主に使用しているプログラミング言語 (Visual C#、Visual Basic など) にクエリ機能が統合されます。LINQ により、これまではプログラミング コードでしか利用できなかった、リッチ メタデータ、コンパイル時構文チェック、静的な型指定、IntelliSense などのメリットを "クエリ式" で利用できるようになります。また、LINQ という宣言型の単一汎用クエリ機能により、外部ソースからの情報だけでなく、メモリ内のすべての情報に対してクエリ機能を利用することもできます。
.NET LINQ では、一連の汎用 "標準クエリ演算子" が定義されます。この標準クエリ演算子により、.NET ベースのプログラミング言語内で宣言型の直接的手法を使って、トラバーサル (一括) 演算、フィルタ演算、およびプロジェクション演算を表現できます。標準クエリ演算子では、IEnumerable<T> ベースのすべての情報ソースにクエリを適用できます。LINQ の採用により、サード パーティから提供される標準クエリ演算子のセットが増加します。こうした標準クエリ演算子の中には、対象ドメインや対象テクノロジに対応する新しいドメイン固有の演算子も含まれます。さらに重要な点は、サード パーティが標準クエリ演算子を独自の実装に自由に置き換えられることです。サード パーティ独自の実装では、リモートの評価、クエリの翻訳、最適化などのサービスを追加することができます。このような実装では、"LINQ パターン" の表記法に準拠することで、標準クエリ演算子と同じ統合言語とツール サポートを利用できます。
こうしたクエリ アーキテクチャの拡張性は LINQ プロジェクト自体でも使用されており、XML データと SQL データの両方で機能する実装を提供しています。XML に対するクエリ演算子 (LINQ to XML) では、効率的で使いやすい、インメモリ XML 機能を使用して、ホスト プログラミング言語に XPath/XQuery 機能を提供します。リレーショナル データに対するクエリ演算子 (LINQ to SQL) は、共通言語ランタイム (CLR) の型システムに統合された SQL ベースのスキーマ定義を基に構築されています。このような統合により、リレーショナル モデルの表現力や、基になるストアで直接行われるクエリ評価のパフォーマンスを維持しながら、リレーショナル データに厳密な型指定がもたらされます。
標準クエリ演算子の概要
LINQ の作用を調べるために、配列の内容の処理に標準クエリ演算子を使用する簡単な C# 3.0 プログラムから見ていくことにしましょう。
using System;
using System.Linq;
using System.Collections.Generic;
class app {
static void Main() {
string[] names = { "Burke", "Connor", "Frank",
"Everett", "Albert", "George",
"Harris", "David" };
IEnumerable<string> query = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
foreach (string item in query)
Console.WriteLine(item);
}
}
このプログラムをコンパイルして実行すると、次のような出力が得られます。
BURKE
DAVID
FRANK
LINQ のしくみを理解するには、プログラムの最初のステートメントを詳しく調べる必要があります。
IEnumerable<string> query = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
ここでは、ローカル変数 query が "クエリ式" で初期化されています。クエリ式は、標準クエリ演算子またはドメイン固有の演算子のいずれかのクエリ演算子を 1 つ以上適用して、1 つ以上の情報ソースに対して演算を行います。この式では、3 つの標準クエリ演算子、Where、OrderBy、および Select を使用します。
Visual Basic 9.0 でも同様に LINQ がサポートされます。上記のステートメントを Visual Basic 9.0 で記述すると次のようになります。
Dim query As IEnumerable(Of String) = From s in names _
Where s.Length = 5 _
Order By s _
Select s.ToUpper()
ここで示した C# と Visual Basic のステートメントはどちらもクエリ式を使用します。foreach ステートメントと同様に、クエリ式は、手作業でのコード記述に代わる、便利な宣言型の簡易表現です。上記のステートメントは、C# で示した次の明確な構文と同じ意味になります。
IEnumerable<string> query = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
この形式のクエリを "メソッドベース" のクエリと呼びます。Where 演算子、OrderBy 演算子、および Select 演算子の引数を "ラムダ式" と呼びます。これは、デリゲートによく似たコード フラグメントです。標準クエリ演算子は、このラムダ式により個別のメソッドとして定義され、ドット表記を使用して連結できるようになります。同時に、これらのメソッドが、拡張性のあるクエリ言語の基礎を形成します。
LINQ プロジェクトをサポートする言語機能
LINQ は、C# 3.0 と Visual Basic 9.0 に新しく導入されたいくつかの汎用言語機能に全面的に基づいて構築されています。これらの機能は単独でも効果がありますが、こうした機能を組み合わせることで、クエリやクエリ可能な API の定義に拡張性の高い方法がもたらされます。ここでは、このような言語機能について詳しく調べ、これらの機能が宣言型の直接的なスタイルのクエリにどのように貢献しているかについて説明します。
ラムダ式と式のツリー
ユーザーは、多くのクエリ演算子を使用して、フィルタ処理、プロジェクション、キー抽出などを実行する関数を提供できます。クエリ機能はラムダ式の概念を基に構築されており、開発者はこの機能を使用して、後続の評価に引数として渡すことができる関数を簡単に記述できます。ラムダ式は CLR のデリゲートに似ており、デリゲート型によって定義されるメソッド シグネチャに従う必要があります。このことを示すために、上記のステートメントを拡張し、同等の機能を Func デリゲート型を使用する、より明確な形式に変換します。
Func<string, bool> filter = s => s.Length == 5;
Func<string, string> extract = s => s;
Func<string, string> project = s => s.ToUpper();
IEnumerable<string> query = names.Where(filter)
.OrderBy(extract)
.Select(project);
ラムダ式は、C# 2.0 の匿名メソッドを自然な形で進化させたものです。たとえば、匿名メソッドを使用すると、上記の例を次のように書き直すことができます。
Func<string, bool> filter = delegate (string s) {
return s.Length == 5;
};
Func<string, string> extract = delegate (string s) {
return s;
};
Func<string, string> project = delegate (string s) {
return s.ToUpper();
};
IEnumerable<string> query = names.Where(filter)
.OrderBy(extract)
.Select(project);
一般に、開発者は名前付きメソッド、匿名メソッド、またはラムダ式をクエリ演算子と自由に併用できます。ラムダ式には、コードの記述に最も直接的で複雑な構文を利用できるメリットがあります。さらに重要なことに、ラムダ式はコードとしてもデータとしてもコンパイルできます。そのため、オプティマイザ、トランスレータ、およびエバリュエータによりラムダ式を実行時に処理することができます。
System.Linq.Expressions 名前空間では、よく知られた汎用型の Expression<T> が定義されます。この型は、特定のラムダ式に、従来の IL ベースのメソッド本体ではなく、"式のツリー" が求められていることを示します。式のツリーはラムダ式の効率的なメモリ内表現で、式に依存しない明確な構造を作成します。
コンパイラが実行可能な IL を生成するか、式のツリーを生成するかは、ラムダ式の使い方によって決まります。型がデリゲートである変数、フィールド、またはパラメータにラムダ式が代入される場合、コンパイラは匿名メソッドと同等の IL を生成します。あるデリゲート型 T に対して、型が Expression<T> である変数、フィールド、またはパラメータにラムダ式が代入される場合、コンパイラは式のツリーを生成します。
たとえば、次の 2 つの変数宣言を考えてみましょう。
Func<int, bool> f = n => n < 5; Expression<Func<int, bool>> e = n => n < 5;
変数 f はデリゲートへの参照で、直接実行できます。
bool isSmall = f(2); // isSmall は True になります。
変数 e は式のツリーへの参照で、直接実行できません。
bool isSmall = e(2); // コンパイル エラー、式はデータです。
デリゲートは実質的には内容が明確にされていないコードですが、式のツリーはプログラム内から他の任意のデータ構造とまったく同様にアクセスすることができます。
Expression<Func<int, bool>> filter = n => n < 5;
BinaryExpression body = (BinaryExpression)filter.Body;
ParameterExpression left = (ParameterExpression)body.Left;
ConstantExpression right = (ConstantExpression)body.Right;
Console.WriteLine("{0} {1} {2}",
left.Name, body.NodeType, right.Value);
上記の例は、実行時に式のツリーを分解し、次の文字列を出力します。
n LessThan 5
このように実行時に式のツリーをデータとして扱う機能は、プラットフォームの一部であるベース クエリの抽象化を利用するサード パーティ製ライブラリのエコシステムを育成する場合に重要です。LINQ to SQL データ アクセスの実装はこの機能を使用して、式のツリーをストア内での評価に適切な T-SQL ステートメントに変換します。
拡張メソッド
ラムダ式はクエリ アーキテクチャの重要な要素の 1 つです。もう 1 つの重要な要素が "拡張メソッド" です。拡張メソッドとは、動的言語のコミュニティでは一般的になった "duck typing" の柔軟性と、静的に型指定される言語のパフォーマンスとコンパイル時検証を組み合わせたものです。サード パーティは拡張メソッドを使用して、新しいメソッドで型のパブリック コントラクトを補強すると同時に、個々の型の作成者はこれらのメソッドの独自の特殊な実装を提供することができます。
拡張メソッドは、静的クラス内で静的メソッドとして定義されます。ただし、CLR メタデータ内に [System.Runtime.CompilerServices.Extension] 属性が付けられます。言語では、拡張メソッド用の直接構文を用意することをお勧めします。C# では、拡張メソッドが this 修飾子で示されます。この修飾子は、拡張メソッドの最初のパラメータに適用する必要があります。では、最も単純なクエリ演算子 Where の定義を見てみましょう。
namespace System.Linq {
using System;
using System.Collections.Generic;
public static class Enumerable {
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, bool> predicate) {
foreach (T item in source)
if (predicate(item))
yield return item;
}
}
}
拡張メソッドの最初のパラメータは、どの型を拡張するかを示します。上記の例の Where 拡張メソッドは、型 IEnumerable<T> を拡張します。Where は静的メソッドなので、他のすべての静的メソッドとまったく同様に直接呼び出すことができます。
IEnumerable<string> query = Enumerable.Where(names,
s => s.Length < 6);
しかし、拡張メソッドが独特なのは、次のようにインスタンス構文を使用しても呼び出せる点です。
IEnumerable<string> query = names.Where(s => s.Length < 6);
拡張メソッドは、どの拡張メソッドがスコープ内になるかを基に、コンパイル時に解決されます。C# の using ステートメントまたは Visual Basic の Import ステートメントを使用して名前空間がインポートされると、その名前空間からの静的クラスによって定義されるすべての拡張メソッドがスコープ内に入ります。
標準クエリ演算子は、型 System.Linq.Enumerable の拡張メソッドとして定義されます。標準クエリ演算子を調べてみると、わずかな例外を除くすべての演算子が IEnumerable<T> インターフェイスに関連して定義されていることがわかります。つまり、C# で次の using ステートメントを追加するだけで、あらゆる IEnumerable<T> 互換の情報ソースが標準クエリ演算子を取得します。
using System.Linq; // クエリ演算子を利用可能にします。
特定の型の標準クエリ演算子を置き換えたい場合、その型の同じ名前のメソッドを互換性のあるシグネチャで独自に定義するか、その型を拡張する拡張メソッドを同じ名前で新たに定義します。標準クエリ演算子が同時に利用可能になることを避けたい場合は、単純に System.Linq がスコープ内に入らないようにし、IEnumerable<T> に対して独自の拡張メソッドを記述します。
拡張メソッドは解決の優先順位が最も低く、対象の型とその基本型にふさわしい組み合わせがない場合にのみ使用されます。そのため、ユーザー定義型を使用して、標準演算子よりも優先度が高い、独自のクエリ演算子を提供できます。たとえば、次のカスタム コレクションを考えてみましょう。
public class MySequence : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
for (int i = 1; i <= 10; i++)
yield return i;
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
public IEnumerable<int> Where(Func<int, bool> filter) {
for (int i = 1; i <= 10; i++)
if (filter(i))
yield return i;
}
}
このクラス定義を前提とすると、次のプログラムは MySequence.Where の実装を、拡張メソッドとしてではなく、拡張メソッドよりも優先度の高い、インスタンス メソッドとして使用します。
MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
Console.WriteLine(item);
OfType 演算子は標準演算子ですが、IEnumerable<T> ベースの情報ソースを拡張しない数少ない演算子での 1 つです。では、この OfType クエリ演算子を見てみましょう。
public static IEnumerable<T> OfType<T>(this IEnumerable source) {
foreach (object item in source)
if (item is T)
yield return (T)item;
}
OfType は IEnumerable<T> ベースのソースだけでなく、.NET Framework バージョン 1.0 に存在していた、パラメータ化されない IEnumerable インターフェイスに対して記述されたソースも受け取ります。OfType 演算子を使用して、次のように従来の .NET コレクションに標準クエリ演算子を適用することができます。
// "classic" はクエリ演算子と直接併用できません。 IEnumerable classic = new OlderCollectionType(); // "modern" はクエリ演算子と直接併用できます。 IEnumerable<object> modern = classic.OfType<object>();
この例では、変数 modern は classic と同じ値のシーケンスを生成します。ただし、その型は最新の IEnumerable<T> コードと互換性があり、標準クエリ演算子を含みます。
OfType 演算子では型を基にソースからの値をフィルタ処理するため、情報ソースが新しくなった場合でも役に立ちます。OfType は新しいシーケンスを作成するときに、型引数と互換性のないメンバを単純に元のシーケンスから除外します。異種配列から文字列を抽出する次の簡単なプログラムを考えます。
object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();
foreach ステートメントで justStrings 変数を列挙すると、2 つの文字列 "Hello" と "World" のシーケンスが取得されます。
クエリの遅延評価
観察力の鋭い読者なら、標準 Where 演算子が、C# 2.0 で導入された yield 構造を使用して実装されていることに気付いたかもしれません。この実装手法は、値のシーケンスを返す標準演算子すべてに共通して使用されます。yield を使用するメリットは、foreach ステートメントを使用したり、基になる GetEnumerator メソッドと MoveNext メソッドを手動で使用したりして、実際に繰り返し処理が行われるまでクエリが評価されない点にあります。このような遅延評価では、クエリを、複数回評価できる IEnumerable<T> ベースの値として保持することができるため、毎回異なる結果が生成される可能性があります。
多くのアプリケーションでは、この動作はまったく問題になりません。ただし、クエリ評価の結果をキャッシュするようなアプリケーションでは、クエリの即時評価を実行できる 2 つの演算子 ToList と ToArray が用意されています。どちらもクエリ評価の結果を返しますが、前者は List<T> を返し、後者は配列を返します。
クエリの遅延評価の動作を調べるために、配列に対して簡単なクエリを実行する次のプログラムを考えてみましょう。
// 文字列をいくつか保持する変数を宣言します。
string[] names = { "Allen", "Arthur", "Bennett" };
// クエリを表す変数を宣言します。
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');
// クエリを評価します。
foreach (string item in ayes)
Console.WriteLine(item);
// 元の情報ソースを変更します。
names[0] = "Bob";
// クエリを再度評価します。この時点では "Allen" が含まれていません。
foreach (string item in ayes)
Console.WriteLine(item);
変数 ayes の繰り返し処理が行われるたびに、クエリが評価されます。結果をキャッシュしたコピーが必要なことを示す場合は、次のようにクエリの最後に ToList 演算子または ToArray 演算子を付加するだけです。
// 文字列をいくつか保持する変数を宣言します。
string[] names = { "Allen", "Arthur", "Bennett" };
// クエリの即時評価の結果を表す
// 変数を宣言します。
string[] ayes = names.Where(s => s[0] == 'A').ToArray();
// キャッシュしたクエリ結果を繰り返し処理します。
foreach (string item in ayes)
Console.WriteLine(item);
// 元のソースを変更しても ayes には影響しません。
names[0] = "Bob";
// 結果を再度繰り返し処理します。依然として "Allen" が含まれます。
foreach (string item in ayes)
Console.WriteLine(item);
ToArray と ToList はどちらもクエリの即時評価を実行します。同様に、単一値を返す標準クエリ演算子 (First、ElementAt、Sum、Average、All、Any など) でもクエリの即時評価が行われます。
IQueryable<T> インターフェイス
通常、LINQ to SQL など、式のツリーを使用してクエリ機能を実装するデータ ソースでも、同様の遅延実行モデルが求められます。このようなデータ ソースには IQueryable<T> インターフェイスを実装することでメリットが得られる場合があります。このインターフェイスでは、LINQ パターンに必要なすべてのクエリ演算子が式のツリーを使用して実装されるためです。各 IQueryable<T> では、"クエリの実行に必要なコード" が式のツリーの形式で表現されます。すべての遅延クエリ演算子は、式のツリーにそのクエリ演算子を呼び出す表記を追加した、新しい IQueryable<T> を返します。その結果、クエリが評価される時点に達すると、IQueryable<T> が列挙されるため、データ ソースは全クエリを 1 つのバッチで表現する式のツリーを処理できます。たとえば、クエリ演算子を数多く呼び出すことによって得られる複雑な LINQ to SQL クエリでも、たった 1 つの SQL クエリとしてデータベースに送信されることになります。
IQueryable<T>
インターフェイスを実装することにより、こうした遅延機能の再利用を実装するデータ ソースのメリットは明らかです。一方、クエリを記述するクライアントにとっては、リモート情報ソースに対して共通の型を所持するという大きなメリットがあります。これにより、異なるデータ ソースに対して使用できるポリモーフィックなクエリを記述できるだけでなく、ドメインをまたがって送信されるクエリを記述できる可能性がもたらされます。
複合値の初期化
値のシーケンスから単純にメンバをフィルタ選択して取り出すクエリに必要な処理はすべて、ラムダ式と拡張メソッドで提供されます。ほとんどのクエリ式では、こうしたメンバに対するプロジェクションも実行し、元のメンバのシーケンスを、値や型が異なるメンバのシーケンスに効果的に変換します。LINQ ではこのような変換をサポートするために、"オブジェクト初期化子" という新しい構造により、構造化された型の新しいインスタンスを作成します。この資料では、これ以降、次の型が定義されているものとして話を進めます。
public class Person {
string name;
int age;
bool canCode;
public string Name {
get { return name; } set { name = value; }
}
public int Age {
get { return age; } set { age = value; }
}
public bool CanCode {
get { return canCode; } set { canCode = value; }
}
}
オブジェクト初期化子を使用すれば、型のパブリック フィールドとプロパティを基に、容易に値を構成できます。たとえば、Person 型の新しい値を作成する場合、次のステートメントを記述できます。
Person value = new Person {
Name = "Chris Smith", Age = 31, CanCode = false
};
このステートメントの意味合いは、次の一連のステートメントと同じです。
Person value = new Person(); value.Name = "Chris Smith"; value.Age = 31; value.CanCode = false;
オブジェクト初期化子では、(ラムダ式内や式のツリー内など) 式のみが許可されるコンテキストで新しく構造化された値を作成できるため、LINQ では重要な機能です。たとえば、入力シーケンスの値ごとに新しく Person 値を作成する次のクエリ式を考えてください。
IEnumerable<Person> query = names.Select(s => new Person {
Name = s, Age = 21, CanCode = s.Length == 5
});
オブジェクト初期化構文は、構造化された値の配列を初期化する場合にも便利です。たとえば、個別のオブジェクト初期化子を使用して初期化される、次の配列変数を考えてください。
static Person[] people = {
new Person { Name="Allen Frances", Age=11, CanCode=false },
new Person { Name="Burke Madison", Age=50, CanCode=true },
new Person { Name="Connor Morgan", Age=59, CanCode=false },
new Person { Name="David Charles", Age=33, CanCode=true },
new Person { Name="Everett Frank", Age=16, CanCode=true },
};
構造化された値と型
LINQ プロジェクトでは、状態と動作の両方を兼ね備えた完全なオブジェクトではなく、主に構造化された値に静的な "形状" を提供することを目的として何らかの型が存在するような、データ中心のプログラミング スタイルをサポートします。このような論理的結論に達する前提を考えると、多くの場合、開発者の関心事項のすべては値の構造にあり、その形状に名前付きの型が必要になることはほとんどない点にあります。これが "匿名型" の導入につながりました。匿名型では、新しい構造を "インライン" で定義でき、定義時に初期化することができます。
C# での匿名型の構文は、型の名前を省略する点を除けば、オブジェクト初期化構文と同じです。たとえば、次の 2 つのステートメントを考えてみます。
object v1 = new Person {
Name = "Brian Smith", Age = 31, CanCode = false
};
object v2 = new { // 型名が省略されている点に注意してください。
Name = "Brian Smith", Age = 31, CanCode = false
};
変数 v1 と v2 はどちらも、3 つのパブリック プロパティ Name、Age、および CanCode を備えた CLR 型のインメモリ オブジェクトを指します。2 つの変数の違いは、v2 が "匿名型" のインスタンスと呼ばれる点です。CLR の用語では、匿名型と他の型には違いがありません。匿名型が特殊なのは、プログラミング言語内で意味のある名前が付けられないことです。匿名型のインスタンスを作成する唯一の方法は、上記の構文を使用することです。
静的な型指定のメリットを活かしながら、変数を匿名型のインスタンスにできるように、C# では "暗黙に型指定されるローカル変数" を導入しました。ローカル変数宣言で型名の代わりに、var キーワードを使用できます。たとえば、次の正規の C# 3.0 プログラムを考えます。
var s = "Bob"; var n = 32; var b = true;
var キーワードは、変数の初期化に使用している式の静的な型から変数の型を推測するようにコンパイラに指示します。この例では、s、n、および b の型はそれぞれ string、int、および bool です。このプログラムは、次のプログラムと同じです。
string s = "Bob"; int n = 32; bool b = true;
var キーワードは、意味のある名前を持つ型の変数に便利ですが、匿名型のインスタンスを表す変数にも不可欠です。
var value = new {
Name = " Brian Smith", Age = 31, CanCode = false
};
上記の例では、変数 value は匿名型で、次の擬似 C# コードと同じ定義になります。
internal class ??? {
string _Name;
int _Age;
bool _CanCode;
public string Name {
get { return _Name; } set { _Name = value; }
}
public int Age{
get { return _Age; } set { _Age = value; }
}
public bool CanCode {
get { return _CanCode; } set { _CanCode = value; }
}
public bool Equals(object obj) { ... }
public bool GetHashCode() { ... }
}
アセンブリが異なる場合、匿名型は共有できません。ただし、各アセンブリ内のプロパティの名前と型の組の特定のシーケンスに最大 1 つの匿名型が存在することがコンパイラにより確認されます。
匿名型は、既存の構造化された値のメンバを 1 つ以上選択するために、プロジェクション内で使用されることが多いため、匿名型の初期化時に、別の値からのフィールドやプロパティを簡単に参照できるようになっています。このような参照により、新しい匿名型には、参照先のプロパティやフィールドから名前、型、および値がすべてコピーされた 1 つのプロパティが含まれることになります。
たとえば、他の値からのプロパティを組み合わせて構造化された値を新しく作成する以下の例を考えてみます。
var bob = new Person { Name = "Bob", Age = 51, CanCode = true };
var jane = new { Age = 29, FirstName = "Jane" };
var couple = new {
Husband = new { bob.Name, bob.Age },
Wife = new { Name = jane.FirstName, jane.Age }
};
int ha = couple.Husband.Age; // ha == 51
string wn = couple.Wife.Name; // wn == "Jane"
上記のフィールドやプロパティへの参照は、単に、以下の明確な形式の記述に対する簡易構文です。
var couple = new {
Husband = new { Name = bob.Name, Age = bob.Age },
Wife = new { Name = jane.FirstName, Age = jane.Age }
};
どちらの場合も、couple 変数は bob と jane から Name プロパティと Age プロパティの独自のコピーを取得します。
匿名型は、クエリの select 句で最も頻繁に使用されます。たとえば、次のクエリを考えてみましょう。
var query = people.Select(p => new {
p.Name, BadCoder = p.Age == 11
});
foreach (var item in query)
Console.WriteLine("{0} is a {1} coder",
item.Name,
item.BadCoder ? "bad" : "good");
この例では、Person 型に対して新しくプロジェクションを作成しています。これにより、静的な型のメリットを活かしながら、コードの処理に必要な形状に正確に一致させることができます。
標準クエリ演算子の詳細
多くの演算子には、上記で説明した基本クエリ機能に加えて、シーケンスの操作やクエリの構成に役立つ手法が用意されており、ユーザーは標準クエリ演算子の簡易フレームワーク内の結果を細かく制御できます。
並べ替えとグループ化
一般に、クエリを評価した結果は、基になる情報ソースに本来備わっている順序で作成された一連の値になります。このように作成された値の順序を開発者が明確に制御できるようにするには、順序を制御するための標準クエリ演算子を定義します。こうした演算子の中でも最も基本的なものが OrderBy 演算子です。
OrderBy 演算子と OrderByDescending 演算子は、任意の情報ソースに適用でき、結果の並べ替えに使用する値を作成するキー抽出関数はユーザーが指定できます。OrderBy 演算子と OrderByDescending 演算子は、キーに部分的な順序を設定するために使用できる、省略可能な比較関数も受け取ります。次の基本的な例を見てください。
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
// 単一の並べ替え
var s1 = names.OrderBy(s => s);
var s2 = names.OrderByDescending(s => s);
// 長さによる並べ替え
var s3 = names.OrderBy(s => s.Length);
var s4 = names.OrderByDescending(s => s.Length);
最初の 2 つのクエリ式は、文字列比較を基に情報ソースのメンバを並べ替えた新しいシーケンスを作成します。次の 2 つのクエリ式は、各文字列の長さを基に情報ソースのメンバを並べ替えた新しいシーケンスを作成します。
OrderBy と OrderByDescending はどちらも並べ替え条件を複数指定できるように、汎用の IEnumerable<T> ではなく、OrderedSequence<T> を返します。OrderedSequence<T> のみで定義されている演算子は ThenBy と ThenByDescending の 2 つで、追加 (下位) の並べ替え条件を適用します。ThenBy/ThenByDescending 自体も OrderedSequence<T> を返すため、任意の数の ThenBy/ThenByDescending 演算子を適用できます。
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);
この例の s1 が参照するクエリの評価では、次の値のシーケンスが作成されます。
"Burke", "David", "Frank", "Albert", "Connor", "George", "Harris", "Everett"
標準クエリ演算子には、OrderBy ファミリの演算子以外に、Reverse 演算子もあります。Reverse は、単に同じ値のシーケンスを列挙し、それを逆順にします。Reverse は OrderBy とは異なり、順序の決定時に実際の値そのものは考慮せず、単に、基になる情報ソースによって生成された値の順序に依存します。
OrderBy 演算子は、値のシーケンスに並べ替え順を設定します。標準クエリ演算子には、キー抽出関数に基づいて値のシーケンスをパーティション分割する GroupBy 演算子もあります。GroupBy 演算子は、検出した個々のキー値ごとに 1 つ、IGrouping 値のシーケンスを返します。IGrouping は、コンテンツの抽出に使用したキーを追加で含む IEnumerable です。
public interface IGrouping<K, T> : IEnumerable<T> {
public K Key { get; }
}
GroupBy 演算子の最も単純な適用例は次のようになります。
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
// 長さでグループ化
var groups = names.GroupBy(s => s.Length);
foreach (IGrouping<int, string> group in groups) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (string value in group)
Console.WriteLine(" {0}", value);
}
このプログラムを実行すると、次のような出力結果が得られます。
Strings of length 6 Albert Connor George Harris Strings of length 5 Burke David Frank Strings of length 7 Everett
GroupBy では、Select と同様の形式で、グループ メンバの設定に使用するプロジェクション関数を提供できます。
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
// 長さでグループ化
var groups = names.GroupBy(s => s.Length, s => s[0]);
foreach (IGrouping<int, char> group in groups) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (char value in group)
Console.WriteLine(" {0}", value);
}
このように変更することで、次のような出力結果が得られます。
Strings of length 6 A C G H Strings of length 5 B D F Strings of length 7 E
注 この例から、プロジェクションが行われた型と情報ソースの型が同じである必要がないことがわかります。この場合は、整数から文字列のシーケンス内の文字へのグループ化を行いました。
集計演算子
値のシーケンスを 1 つの値に集計するための標準クエリ演算子がいくつか定義されています。最も一般的な集計演算子は Aggregate で、次のように定義されます。
public static U Aggregate<T, U>(this IEnumerable<T> source,
U seed, Func<U, T, U> func) {
U result = seed;
foreach (T element in source)
result = func(result, element);
return result;
}
Aggregate 演算子は、値のシーケンスでの計算の実行を容易にします。Aggregate は、基になるシーケンスのメンバごとに 1 回ラムダ式を呼び出すことによって機能します。Aggregate からラムダ式を呼び出すたびに、シーケンスからのメンバとそれまでの集計値が両方渡されます (初期値は Aggregate への seed パラメータに基づきます)。ラムダ式の結果が直前の集計値に置き換わるため、Aggregate からはラムダ式の最終結果が返ります。
たとえば、次のプログラムは Aggregate を使用して、文字列の配列内の総文字数を累積計算します。
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
int count = names.Aggregate(0, (c, s) => c + s.Length);
// count == 46
標準クエリ演算子には、汎用の Aggregate 演算子以外にも、一般的な集計演算を容易にする、汎用 Count 演算子と 4 つの数値集計演算子 (Min、Max、Sum、および Average) もあります。数値集計関数は、数値型 (int、double、decimal など) の値のシーケンスで機能します。また、シーケンスのメンバから数値型へのプロジェクションを行う関数を用意すれば、任意の値のシーケンスで機能させることができます。
次のプログラムでは、上記の両方の形式で Sum 演算子を使用する例を示します。
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
int total1 = numbers.Sum(); // total1 == 55
int total2 = names.Sum(s => s.Length); // total2 == 46
注 2 番目の Sum ステートメントは、前の Aggregate を使用する例と同じになります。
Select と SelectMany の比較
Select 演算子には、元のシーケンス内の値ごとに 1 つの値を作成する変換関数が必要です。変換関数がそれ自体がシーケンスである値を返す場合、そのサブシーケンス全体を手動で処理するのは使用者側の役割です。たとえば、既存の String.Split メソッドを使用して、文字列をトークンに分割する、次のプログラムを考えてみましょう。
string[] text = { "Albert was here",
"Burke slept late",
"Connor is happy" };
var tokens = text.Select(s => s.Split(' '));
foreach (string[] line in tokens)
foreach (string token in line)
Console.Write("{0}.", token);
このプログラムを実行すると、次のテキストが出力されます。
Albert.was.here.Burke.slept.late.Connor.is.happy.
ただし、使用者側で中間の string[] を用意しないで、クエリからトークンを連結したシーケンスを返すのが理想的です。これを実現するには、Select 演算子の代わりに SelectMany 演算子を使用します。SelectMany 演算子は、Select 演算子と同様に機能します。その後 SelectMany 演算子によって展開されるシーケンスを変換関数から返すことを求められる点が異なります。上記のプログラムを SelectMany を使って書き直してみます。
string[] text = { "Albert was here",
"Burke slept late",
"Connor is happy" };
var tokens = text.SelectMany(s => s.Split(' '));
foreach (string token in tokens)
Console.Write("{0}.", token);
SelectMany を使用すると、通常の評価の一環として、各中間シーケンスが展開されることになります。
SelectMany は 2 つの情報ソースを組み合わせる場合に理想的です。
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.SelectMany(n =>
people.Where(p => n.Equals(p.Name))
);
SelectMany に渡されるラムダ式では、異なるソースに入れ子になったクエリが適用されますが、入れ子になったクエリは外部ソースから渡された n パラメータのスコープ内にあります。そのため、people.Where は、最終出力用に SelectMany によって展開された結果のシーケンスを指定して、各 n に対して 1 回呼び出されます。結果は、names 配列内に出現する名前を持つすべての人のシーケンスになります。
結合演算子
オブジェクト指向のプログラムでは、相互に関連するオブジェクトは、通常、ナビゲートが容易なオブジェクト参照を使ってリンクされます。一般に、外部情報ソースの場合は、同様のことは当てはまりません。外部情報ソースの多くのデータ エントリは、参照先のエンティティを一意に識別できる ID などを使って、シンボリックにお互いを "指す" 以外に選択肢はありません。"結合" とは、あるシーケンスの要素と別のシーケンスからの "対応する" 要素を組み合わせて 1 つにする考え方のことです。
上記の SelectMany の例で実際に行っていることを正確に言えば、人の名前を、名前を集めた文字列配列と照合しています。しかし、SelectMany が採用しているアプローチは、この目的にはあまり効果的ではありません。このアプローチでは、people の要素をすべてループして、要素ごとに names の要素と照合しています。このシナリオのすべての情報 (2 つの情報ソースと照合に使用する "キー") を、次のように Join 演算子という 1 つのメソッド呼び出しにまとめると、作業効率が大幅に向上します。
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.Join(people, n => n, p => p.Name, (n,p) => p);
簡単なコードですが、各部分がどのように構成されているかを見てみましょう。Join メソッドは、"外側" のデータ ソース names で呼び出されています。最初の引数は "内側" のデータ ソース people です。2 番目と 3 番目の引数は、外側と内側のソースの要素からそれぞれキーを抽出するラムダ式です。これらのキーは、Join メソッドで要素の照合に使用されます。ここでは、names 自体を people の Name プロパティと照合します。最後のラムダ式で、結果となる要素のシーケンスが作成されます。このラムダ式は照合する要素 n と p の各組を指定して呼び出され、結果が形成されます。この場合は、n を破棄し、p を返すことを選択しました。最終結果は、names の一覧に含まれる Name を持つ、people の Person 要素の一覧になります。
Join と同様の演算子の中で最も優れているのが GroupJoin 演算子です。GroupJoin は、結果を形成するラムダ式の使用方法が Join とは異なります。ラムダ式は、外側の要素と内側の要素の各組個別に呼び出されるのではなく、外側の要素と照合する内側の全要素のシーケンスを指定して、外側の要素ごとに 1 回だけ呼び出されます。具体的には次のようになります。
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.GroupJoin(people, n => n, p => p.Name,
(n, matching) =>
new { Name = n, Count = matching.Count() }
);
この呼び出しでは、指定した名前から始まり、その名前を持つ人の数とペアにした names のシーケンスが作成されます。そのため、GroupJoin 演算子により、外側の 1 つの要素に対する "照合の全セット" を結果の基礎とすることができます。
クエリ構文
C# の既存の foreach ステートメントでは、.NET Framework の IEnumerable/IEnumerator メソッド上で繰り返し処理を行うための宣言型構文が提供されます。特に foreach ステートメントを使う必要はありませんが、このステートメントは非常に便利で、広く使用される言語メカニズムであることが実証されています。
"クエリ構文" は、こうした前例に基づき、Where、Join、GroupJoin、Select、SelectMany、GroupBy、OrderBy、ThenBy、OrderByDescending、ThenByDescending、 Cast などの最も一般的なクエリ演算子のクエリ式を宣言型の構文を使用して簡素化します。
では、まず、次の簡単なクエリから見ていきましょう。
IEnumerable<string> query = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
クエリ式を使用して、まったく同じステートメントを次のように書き直すことができます。
IEnumerable<string> query = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
クエリ式は、C# の foreach ステートメント同様、よりコンパクトになり、読み取りやすくなっていますが、まったく使用しなくてもかまいません。クエリ式で書き直せるすべての式は、ドット表記を使用する構文に対応しています (やや、冗長にはなります)。
では、クエリ式の基本的な構造を見てみることにしましょう。C# での各クエリ式の構文は、from 句で始まり、select 句または group 句のいずれかで終わります。最初の from 句に続いて from 句、let 句、where 句、join 句、および orderby 句を 0 個以上指定できます。各 from 句はシーケンスを対象とする範囲変数を導入するジェネレータで、各 let 句は式の結果に名前を与え、各 where 句は結果から項目を除外するフィルタです。各 join 句は前の句の結果と新しいデータ ソースを相互に関連付けます。orderby 句は結果の順序を指定します。
query-expression ::= from-clause query-body
query-body ::=
query-body-clause* final-query-clause query-continuation?
query-body-clause ::=
(from-clause
| join-clause
| let-clause
| where-clause
| orderby-clause)
from-clause ::=from itemName in srcExpr
join-clause ::=join itemName in srcExpr on keyExpr equals keyExpr
(into itemName)?
let-clause ::=let itemName = selExpr
where-clause ::= where predExpr
orderby-clause ::= orderby (keyExpr (ascending | descending)?)*
final-query-clause ::=
(select-clause | groupby-clause)
select-clause ::= select selExpr
groupby-clause ::= group selExpr by keyExpr
query-continuation ::= intoitemName query-body
たとえば、次の 2 つのクエリ式を考えてみましょう。
var query1 = from p in people
where p.Age > 20
orderby p.Age descending, p.Name
select new {
p.Name, Senior = p.Age > 30, p.CanCode
};
var query2 = from p in people
where p.Age > 20
orderby p.Age descending, p.Name
group new {
p.Name, Senior = p.Age > 30, p.CanCode
} by p.CanCode;
コンパイラは、これらのクエリ式を以下の明確なドット表記を使用して記述されたかのように扱います。
var query1 = people.Where(p => p.Age > 20)
.OrderByDescending(p => p.Age)
.ThenBy(p => p.Name)
.Select(p => new {
p.Name,
Senior = p.Age > 30,
p.CanCode
});
var query2 = people.Where(p => p.Age > 20)
.OrderByDescending(p => p.Age)
.ThenBy(p => p.Name)
.GroupBy(p => p.CanCode,
p => new {
p.Name,
Senior = p.Age > 30,
p.CanCode
});
クエリ式は、メソッドの呼び出しを具体的な名前に機械的に変換します。クエリ演算子のどの "実装" が選択されるかは、クエリ対象の変数の型とスコープ内の拡張メソッドの両方に依存します。
これまで示してきたクエリ式では、ジェネレータを 1 つしか使用していませんでした。ジェネレータを複数使用するときは、後に続くジェネレータはそれぞれその前のジェネレータのコンテキストで評価されます。たとえば、これまでのクエリに少し手を加えた次のクエリを考えてみます。
var query = from s1 in names
where s1.Length == 5
from s2 in names
where s1 == s2
select s1 + " " + s2;
このクエリを次の入力配列に対して実行します。
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
結果は次のようになります。
Burke Burke Frank Frank David David
上記のクエリ式は、以下のドット表記に展開されます。
var query = names.Where(s1 => s1.Length == 5)
.SelectMany(s1 => names, (s1,s2) => new {s1,s2})
.Where($1 => $1.s1 == $1.s2)
.Select($1 => $1.s1 + " " + $1.s2);
注 このバージョンの SelectMany は、外側のシーケンスと内側のシーケンスからの要素に基づく結果を作成するために使用するラムダ式を追加で受け取ります。このラムダ式では、2 つの範囲変数が 1 つの匿名型にまとめられます。コンパイラでは、後続のラムダ式で匿名型を表すために、変数名 $1 が使用されます。
特殊なジェネレータが join 句です。この句により、特定のキーに従って前の句の要素と照合される、別のソースの要素が導入されます。join 句では一致している要素が 1 つずつ生成されます。ただし、into 句が同時に指定されている場合は、一致している要素がグループとして生成されます。
var query = from n in names
join p in people on n equals p.Name into matching
select new { Name = n, Count = matching.Count() };
当然、このクエリは上記のクエリの 1 つに直接展開されます。
var query = names.GroupJoin(people, n => n, p => p.Name,
(n, matching) =>
new { Name = n, Count = matching.Count() }
);
これは、あるクエリの結果を後に続くクエリのジェネレータとして扱う場合に有効です。この機能をサポートするには、クエリ式で into キーワードを使用し、select 句または group 句の後に新しいクエリ式をつなげます。これを "クエリ継続" と呼びます。
特に、group
by 句の結果の後処理を行う場合に、into キーワードが役に立ちます。たとえば、次のプログラムを考えてみます。
var query = from item in names
orderby item
group item by item.Length into lengthGroups
orderby lengthGroups.Key descending
select lengthGroups;
foreach (var group in query) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (var val in group)
Console.WriteLine(" {0}", val);
}
このプログラムの出力は次のようになります。
Strings of length 7 Everett Strings of length 6 Albert Connor George Harris Strings of length 5 Burke David Frank
ここでは、C# でクエリ式を実装する方法について説明しました。他の言語では、追加のクエリ演算子が明確な構文と共にサポートされている場合もあれば、クエリ式をまったくサポートしない場合もあります。
クエリ構文が標準クエリ演算子に固定的に結び付けられていない点に注意することが重要です。クエリ構文は純粋に構文上の機能で、適切な名前とシグネチャで基になるメソッドを実装することで "クエリ パターン" を満たすすべてのものに適用されます。ここまでに説明した標準クエリ演算子は、IEnumerable<T> インターフェイスを追加した拡張メソッドを使用することでこれを行います。開発者は、クエリ パターンに準拠している限り、必要なメソッドを直接実装するか、拡張メソッドを追加することで、希望する任意の型のクエリ構文を開発できます。
LINQ プロジェクト自体もこうした拡張性を活かして、2 つの "LINQ 対応" の API を提供しています。1 つは LINQ to SQL で、SQL ベースのデータ アクセスの LINQ パターンを実装します。もう 1 つは LINQ to XML で XML データでの LINQ クエリを可能にします。次に、この 2 つについて説明します。
LINQ to SQL: SQL 統合
ローカル プログラミング言語に構文やコンパイル時の環境を用意しなくても、.NET LINQ を使用してリレーショナル データ ストアにクエリを発行できます。この機能 (コードネーム LINQ to SQL) では、CLR メタデータへの SQL スキーマ統合を使用します。この統合により、SQL のテーブル定義とビュー定義が、任意の言語からアクセスできる CLR 型にコンパイルされます。
LINQ to SQL では中核となる 2 つの属性 [Table] と [Column] が定義され、どの CLR 型とプロパティが外部 SQL データに対応するかを示します。[Table] 属性はクラスに適用することができ、CLR 型を名前付きの SQL テーブルやビューに関連付けます。[Column] 属性は任意のフィールドやプロパティに適用することができ、メンバを名前付きの SQL 列に関連付けます。どちらに適用した属性も、SQL 固有のメタデータを保持するためにパラメータ化することができます。たとえば、次の単純な SQL スキーマ定義を考えます。
create table People (
Name nvarchar(32) primary key not null,
Age int not null,
CanCode bit not null
)
create table Orders (
OrderID nvarchar(32) primary key not null,
Customer nvarchar(32) not null,
Amount int
)
CLR では同じ定義が次のようになります。
[Table(Name="People")]
public class Person {
[Column(DbType="nvarchar(32) not null", Id=true)]
public string Name;
[Column]
public int Age;
[Column]
public bool CanCode;
}
[Table(Name="Orders")]
public class Order {
[Column(DbType="nvarchar(32) not null", Id=true)]
public string OrderID;
[Column(DbType="nvarchar(32) not null")]
public string Customer;
[Column]
public int? Amount;
}
注 この例から、NULL 値を許容する列は CLR の NULL 値を許容する型にマップされることに注意してください。NULL 値を許容する型は .NET Framework バージョン 2.0 で初めて導入されました。そのため、SQL 型に一対一に対応する CLR 型 (nvarchar、char、text など) がない場合は、元の SQL 型が CLR メタデータに保持されます。
リレーショナル ストアにクエリを発行する場合、LINQ パターンの LINQ to SQL 実装では、クエリが式のツリー形式から SQL 式とリモート評価に適した ADO.NET DbCommand オブジェクトに変換されます。たとえば、次の簡単なクエリを考えてみます。
// ADO.NET sql 接続上にクエリ コンテキストを確立します。
DataContext context = new DataContext(
"Initial Catalog=petdb;Integrated Security=sspi");
// Person と Order の各 CLR 型に対応する
// リモート テーブルを表す変数を設定します。
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders = context.GetTable<Order>();
// クエリをビルドします。
var query = from c in custs
from o in orders
where o.Customer == c.Name
select new {
c.Name,
o.OrderID,
o.Amount,
c.Age
};
// クエリを実行します。
foreach (var item in query)
Console.WriteLine("{0} {1} {2} {3}",
item.Name, item.OrderID,
item.Amount, item.Age);
DataContext 型は、標準クエリ演算子を SQL に変換する簡易トランスレータを提供します。DataContext ではストアへのアクセスに既存の ADO.NET IDbConnection を使用します。これは、確立済みの ADO.NET 接続オブジェクトまたは接続に使用できる接続文字列のいずれかで初期化できます。
GetTable メソッドでは、リモート テーブルやビューを表すためにクエリ式内で使用できる IEnumerable 互換の変数が提供されます。GetTable を呼び出してもデータベースにアクセスするわけではありません。どちらかと言えば、クエリ式を使用してリモート テーブルやビューにアクセスする "可能性" を表します。上記の例では、プログラムからクエリ式で繰り返し処理を行う (この例では、C# の foreach) まで、クエリがストアに送信されません。プログラムからクエリの最初の繰り返し処理が行われると、DataContext により、ストアに送信される以下の SQL ステートメントが式のツリーから機械的に変換されます。
SELECT [t0].[Age], [t1].[Amount],
[t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]
クエリ機能をローカル プログラミング言語に組み込むことで、開発者は CLR 型とのリレーションシップを静的に確立する必要なしに、リレーショナル モデルの全機能を利用できる点に注意することが重要です。つまり、包括的なオブジェクトとリレーショナル モデルのマッピングにより、こうした機能を待望しているユーザーもこのようなクエリの中核機能を利用できるようになります。LINQ to SQL では、開発者がオブジェクト間のリレーションシップを定義およびナビゲートできる機能を備えた、オブジェクトとリレーショナル モデルのマッピング機能が提供されます。マッピングを使用すると、Orders を Customer クラスのプロパティとして参照できるため、2 つを相互に結び付ける明示的な結合は必要ありません。マッピングの機能を強化するために、外部マッピング ファイルを使用することにより、オブジェクト モデルとマッピングを分離することができます。
LINQ to XML: XML 統合
XML 向けの .NET LINQ (LINQ to XML) では、標準クエリ演算子と、先祖ノード、子孫ノード、兄弟ノードの XPath 形式のナビゲーションを提供するツリー固有の演算子を使用して、XML データのクエリが可能になります。XLinq では、既存の System.Xml リーダー/ライター インフラストラクチャに統合され、W3C DOM よりも使いやすい、XML の効率的なインメモリ表現が提供されます。XML とクエリを統合する作業の大半を行う型として、XName、XElement、および XAttribute の 3 つがあります。
XName では、要素名と属性名の両方に使用される、名前空間で修飾された識別子 (QNames) を扱う使いやすい方法が提供されます。XName では、ユーザーが意識しないでも識別子のアトミック化が効率的に処理され、QName を必要とする任意の場所にシンボルまたはプレーン文字列のいずれかを使用できます。
XML の要素と属性は、それぞれ XElement と XAttribute を使用して表されます。XElement と XAttribute では標準の構築構文がサポートされるため、開発者は自然な構文を使用して XML 表記を記述できます。
var e = new XElement("Person",
new XAttribute("CanCode", true),
new XElement("Name", "Loren David"),
new XElement("Age", 31));
var s = e.ToString();
上記の構文は、以下の XML に対応します。
<Person CanCode="true"> <Name>Loren David</Name> <Age>31</Age> </Person>
XML 表記の作成に DOM ベースのファクトリ パターンが必要なくなり、ToString の実装によりテキスト形式の XML が生成されることに注目してください。次のように、既存の XmlReader や文字列リテラルからも XML 要素を構築できます。
var e2 = XElement.Load(xmlReader); var e1 = XElement.Parse( @"<Person CanCode='true'> <Name>Loren David</Name> <Age>31</Age> </Person>");
XElement では、既存の XmlWriter 型を使用した XML の発行もサポートします。
XElement にはクエリ演算子が効果的に結合されるため、開発者は XML 以外の情報に対するクエリを記述でき、Select 句本体で XElements を構築することにより、結果を XML で生成することができます。
var query = from p in people
where p.CanCode
select new XElement("Person",
new XAttribute("Age", p.Age),
p.Name);
上記のクエリは XElements のシーケンスを返します。この種のクエリ結果から XElements の構築を可能にするために、XElement コンストラクタの引数に直接要素のシーケンスを渡すことができます。
var x = new XElement("People",
from p in people
where p.CanCode
select
new XElement("Person",
new XAttribute("Age", p.Age),
p.Name));
この XML 表記は結果的に次のような XML になります。
<People> <Person Age="11">Allen Frances</Person> <Person Age="59">Connor Morgan</Person> </People>
上記の XML は Visual Basic に直接変換されています。ただし、Visual Basic 9.0 では XML リテラルの使用もサポートされます。XML リテラルでは、Visual Basic から宣言型の XML 構文を直接使用して、クエリ式を表記できます。上記の例は、次の Visual Basic ステートメントを使って構築できます。
Dim x = _
<People>
<%= From p In people __
Where p.CanCode _
Select <Person Age=<%= p.Age %>>p.Name</Person> _
%>
</People>
ここまでの例では、LINQ を使用して新しく XML 値を "構築" する方法を示しました。XElement 型と XAttribute 型により、XML 構造からの情報の "抽出" も容易になります。XElement には、従来の XPath 軸にクエリ式を適用できるアクセサ メソッドが用意されています。たとえば、次のクエリは上記の XElement から名前だけを抽出します。
IEnumerable<string> justNames =
from e in x.Descendants("Person")
select e.Value;
//justNames = ["Allen Frances", "Connor Morgan"]
XML から構造化された値を抽出するには、次のように Select 句で単純にオブジェクト初期化子を使用します。
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int)e.Attribute("Age")
};
XAttribute と XElement はどちらも、テキスト値をプリミティブ型として抽出するために明示的な変換をサポートします。見つからなかったデータを処理する場合は、単純に NULL 値を許容する型にキャストできます。
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int?)e.Attribute("Age") ?? 21
};
この場合は、Age 属性が見つからなかったときに既定値として 21 が使用されます。
Visual Basic 9.0 では XElement のアクセサ メソッドとして Elements、Attribute、および Descendants が言語内で直接サポートされるため、XML 軸プロパティという、さらにコンパクトな直接構文を使用して、XML ベースのデータにアクセスできます。前述の C# ステートメントは、この機能を使用すると、次のように記述できます。
Dim persons = _
From e In x...<Person> _
Select new Person { _
.Name = e.Value, _
.Age = IIF(e.@Age, 21) _
}
Visual Basic では、x...<Person> で名前が Person の x の子孫コレクションがすべて取得され、式 e.@Age で名前が Age の XAttributes がすべて検索されます。
Value プロパティは、コレクション内の最初の属性を取得し、その属性の Value プロパティを呼び出します。
まとめ
.NET LINQ では、CLR と CLR が対象とする言語にクエリ機能が追加されます。クエリ機能はラムダ式と式のツリーを基に構築され、ユーザーには明らかにされない実行可能コードとして、またはダウンストリームの処理や変換に適した、ユーザーには意識されないインメモリ データとして、述語、プロジェクション、およびキー抽出式を使用できます。LINQ プロジェクトで定義される標準クエリ演算子は、すべての IEnumerable<T> ベースの情報ソースで機能します。また、ADO.NET (LINQ to SQL) や System.Xml (LINQ to XML) に統合されるため、リレーショナル データや XML データの処理に LINQ のメリットを活かすことができます。
標準クエリ演算子一覧
| 演算子 | 説明 |
|---|---|
| Where | 述語関数に基づく制限演算子 |
| Select/SelectMany | 選択関数に基づくプロジェクション演算子 |
| Take/Skip/ TakeWhile/SkipWhile | 位置決め関数または述語関数に基づくパーティション分割演算子 |
| Join/GroupJoin | キー選択関数に基づく結合演算子 |
| Concat | 連結演算子 |
| OrderBy/ThenBy/OrderByDescending/ThenByDescending | 省略可能なキー選択関数と比較関数に基づいて昇順または降順に並べ替える並べ替え演算子 |
| Reverse | シーケンスの順序を反転する並べ替え演算子 |
| GroupBy | 省略可能なキー選択関数と比較関数に基づくグループ化演算子 |
| Distinct | 重複を削除するセット演算子 |
| Union/Intersect | 和集合または積集合を返すセット演算子 |
| Except | 差集合を返すセット演算子 |
| AsEnumerable | IEnumerable<T> への変換演算子 |
| ToArray/ToList | 配列または List<T> への変換演算子 |
| ToDictionary/ToLookup | キー選択関数に基づく Dictionary<K,T> または Lookup<K,T> (多重辞書) への変換演算子 |
| OfType/Cast | フィルタ選択に基づく IEnumerable<T> への変換演算子または型引数への変換 |
| SequenceEqual | 対になった要素の等値性をチェックする等価演算子 |
| First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault | 省略可能な述語関数に基づいて初期要素、最終要素、または唯一の要素を返す要素演算子 |
| ElementAt/ElementAtOrDefault | 位置に基づいて要素を返す要素演算子 |
| DefaultIfEmpty | 空のシーケンスを既定値の単一シーケンスに置き換える要素演算子 |
| Range | 範囲内の数を返す生成演算子 |
| Repeat | 特定の値の複数の出現を返す生成演算子 |
| Empty | 空のシーケンスを返す生成演算子 |
| All/Any | 述語関数の実存的または普遍的充足度をチェックする限定子 |
| Contains | 特定の要素の存在をチェックする限定子 |
| Count/LongCount | 省略可能な述語関数に基づいて要素を数える集計演算子 |
| Sum/Min/Max/Average | 省略可能な選択関数に基づく集計演算子 |
| Aggregate | 累積関数および省略可能なシードを基に複数の値を集積する集計演算子 |