はじめに
こんにちは。私は社会人2年目C#er。初心者です。
EntityFrameworkなどが用意するデータモデルはIQueryable<T>
型を返しますが、同様にコレクションを扱うIEnumerable<T>
型とどう異なっているのかよくわかっていませんでした。
このStackOverFlowの回答やこちらの記事を読むことで違いの概要は把握することができたのですが、.NETの実装も踏まえた詳細を理解したかったのでコードを書きながら調査しました。
本記事に関連するサンプルコードは私のGitHub上のこちらにあがっています。
TL;DR
IQueryableはIEnumerableのインターフェースを継承していて、foreach時やToListしたときの"振る舞い"は同じ。
そもそもIQueryableは外部データソース上を扱うLinqプロバイダのために用意されている。
IEnumerable<T>
はクエリ発行がプログラム内部のインメモリ上。一方で、IQueryable<T>
のクエリ発行は外部リソースのプログラム上(RDBMSやAPIサーバ)。
この違いは.NET実装上ではWhere
などのLinqメソッドにて確認することができる。
作って比較してみる
動作を比較するためにIEnumerable<T>
とIQueryable<T>
を実装してみます。
本記事では両者のLinqメソッド実行時の様子を比較するので直接関係の無い部分は作り込んでいません。
IEnumerable<T>
の実装
public class MyEnumerable : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
return this.ReturnEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private IEnumerator<int> ReturnEnumerator()
{
var intArray = new int[] {4, 8, 7, 0, 6, 5, 1, 9, 3, 2};
foreach (var val in intArray)
{
yield return val;
}
}
}
本サンプルコードではIEnumerable<int>
を対象としイテレータ(IEnumerator
)もint型対応です。
yield return
を持つメソッドをイテレータとしています。
IQueryable<T>
の実装
こちらの記事を参考に実装しました。
以下のクラスQuery
はIQueryable<T>
を直接実装するクラスです。
public class Query<T> : IQueryable<T>, IQueryable, IEnumerable<T>, IEnumerable, IOrderedQueryable<T>, IOrderedQueryable
{
QueryProvider provider;
Expression expression;
public Query(QueryProvider provider)
{
if (provider == null)
{
throw new ArgumentNullException("provider");
}
this.provider = provider;
this.expression = Expression.Constant(this);
}
public Query(QueryProvider provider, Expression expression)
{
if (provider == null) {
throw new ArgumentNullException("provider");
}
if (expression == null) {
throw new ArgumentNullException("expression");
}
this.provider = provider;
this.expression = expression;
}
Expression IQueryable.Expression
{
get { return this.expression; }
}
Type IQueryable.ElementType
{
get { return typeof(T); }
}
IQueryProvider IQueryable.Provider
{
get { return this.provider; }
}
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)this.provider.Execute(this.expression)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)this.provider.Execute(this.expression)).GetEnumerator();
}
public override string ToString()
{
return this.provider.GetQueryText(this.expression);
}
}
[ポイント1]
IQueryable
を実装するには下記メンバーを用意する必要がある。
Expression IQueryable.Expression
Type IQueryable.ElementType
IQueryProvider IQueryable.Provider
[ポイント2]
IQueryable
はIEnumerable
を継承するのでこのクラスもGetEnumerator
を実装する必要がある。実装上はIQueryProvider
が持つExecute
メソッドを呼ぶことでイテレータが返る。
本記事の煩雑を避けるため、IQueryProvider
を実装したコードの記載は割愛します。
対象コードのリンクは以下になります。
=> QueryProvider
=> MyQueryProvider
IQueryable
は与えたれたLinq式から式木(IQueryable.Expression
)を用意し、その式木を使ってIQueryable.Provider
がデータソースに対するクエリを生成し発行します。
Linqメソッド実行時の動作を比較
以下のコードを使って上記で作成したIEnumerable<T>
とIQueryable<T>
の実装クラスを比較してみます。
static class Program
{
static void Main()
{
Console.WriteLine($"========== Start {nameof(UseMyEnumerable)} ==========");
UseMyEnumerable();
Console.WriteLine($"========== Start {nameof(UseMyQueryProvider)} ==========");
UseMyQueryProvider();
}
private static void UseMyEnumerable()
{
var e = new MyEnumerable();
var q2 = e.Where(x => x % 2 == 0);
var q3 = q2.Select(x => x * x);
foreach (var val in q3)
{
Console.WriteLine($"{val}");
}
}
private static void UseMyQueryProvider()
{
var q1 = MyQueryProvider.CreateQueryable<int>();
Console.WriteLine($"q1: {q1.Expression}");
var q2 = q1.Where(x => x % 2 == 0);
Console.WriteLine($"q2: {q2.Expression}");
var q3 = q2.Select(x => x * x);
Console.WriteLine($"q3: {q3.Expression}");
// Executeメソッドがnullのみを返すのでこのforeachで"ランタイム"エラーとなる。
// foreachのコメントアウトを外してもコンパイルは通る。
//foreach (var val in q3)
//{
// Console.WriteLine($"{val}");
//}
}
}
実行結果
========== Start UseMyEnumerable ==========
16
64
0
36
4
========== Start UseMyQueryProvider ==========
q1:
q2: .Where(x => ((x % 2) == 0))
q3: .Where(x => ((x % 2) == 0)).Select(x => (x * x))
IQueryable<T>
はLinqメソッドが呼ばれるたびに式木(Expression)が更新されています。コード上のコメントにもあるように、本記事でのIQueryable<T>
実装クラスのIQueryProvider
のExecute
メソッドはnullしか返さないのでforeach時にはNullReferenceのランタイムエラーが発生します。
IEnumerable<T>
実装クラスの内部をみてみる
上記の比較用コードをデバッグして内部を見てみます。
こちらのスクショの赤枠からわかるように、IEnumerable<T>
ではWhere
のLinqメソッド実行時には実態のコレクションがインメモリ上に展開されます。
IEnumerable<T>
は処理自体は遅延実行であり利用者側のメモリには直ちに評価され展開されませんが、元のコレクションは内部的にはLinqメソッド実行時には保持されます。
IQueryable<T>
実装クラスの内部をみてみる
IQueryable<T>
はLinqメソッド実行時はあくまでクエリが構築されるだけのようです。
foreach
などの評価で初めてクエリが外部ソースに発行され結果が取得されるので、IQueryable<T>
は遅延"読み込み"(lazy loading)の特徴があると説明されます。
.NETのソースをみてみる
Enumerable
のWhereに対応するメソッドのソースコードからわかるように、インメモリ上に自身が持つ反復子を取り出しています。
一方でIQueryable
のWhere
メソッドのソースコードを見る限り、IQeuryProvider.CreateQuery
を使って式木を更新しているだけのようです。
おわりに
IQueryable<T>
は外部リソースを扱うLinqプロバイダのために提供されているもので、IEnumerable<T>
とIQueryable<T>
は、クエリ発行がプログラムのインメモリ上か外部リソースのプログラム上かという点で異なっていました。
そして、.NETの実装としてその違いは、Where句などのLinqメソッドで確認することができました。