search
LoginSignup
13

More than 3 years have passed since last update.

posted at

updated at

Organization

結局IEnumerable<T>とIQueryable<T>はどう違うの?

はじめに

こんにちは。私は社会人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>の実装

MyEnumerable.cs
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>の実装

こちらの記事を参考に実装しました。

以下のクラスQueryIQueryable<T>を直接実装するクラスです。

Query.cs
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]
IQueryableIEnumerableを継承するのでこのクラスも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>実装クラスのIQueryProviderExecuteメソッドはnullしか返さないのでforeach時にはNullReferenceのランタイムエラーが発生します。

IEnumerable<T>実装クラスの内部をみてみる

上記の比較用コードをデバッグして内部を見てみます。

IEnumerable_Debugging.png

こちらのスクショの赤枠からわかるように、IEnumerable<T>ではWhereのLinqメソッド実行時には実態のコレクションがインメモリ上に展開されます。
IEnumerable<T>は処理自体は遅延実行であり利用者側のメモリには直ちに評価され展開されませんが、元のコレクションは内部的にはLinqメソッド実行時には保持されます。

IQueryable<T>実装クラスの内部をみてみる

IQueryable_Debugging.png

IQueryable<T>はLinqメソッド実行時はあくまでクエリが構築されるだけのようです。
foreachなどの評価で初めてクエリが外部ソースに発行され結果が取得されるので、IQueryable<T>は遅延"読み込み"(lazy loading)の特徴があると説明されます。

.NETのソースをみてみる

EnumerableのWhereに対応するメソッドのソースコードからわかるように、インメモリ上に自身が持つ反復子を取り出しています。

一方でIQueryableWhereメソッドのソースコードを見る限り、IQeuryProvider.CreateQueryを使って式木を更新しているだけのようです。

おわりに

IQueryable<T>は外部リソースを扱うLinqプロバイダのために提供されているもので、IEnumerable<T>IQueryable<T>は、クエリ発行がプログラムのインメモリ上か外部リソースのプログラム上かという点で異なっていました。

そして、.NETの実装としてその違いは、Where句などのLinqメソッドで確認することができました。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
13