打造自己的LINQ Provider(中):IQueryable和IQueryProvider

概述

在.NET Framework 3.5中提供了LINQ 支持後,LINQ就以其強大而優雅的編程方式贏得了開發人員的喜愛,而各種LINQ Provider更是滿天飛,如LINQ to NHibernate、LINQ to Google等,大有“一切皆LINQ”的趨勢。LINQ本身也提供了很好的擴展性,使得我們可以輕鬆的編寫屬於自己的LINQ Provider。
本文爲打造自己的LINQ Provider系列文章第二篇,主要詳細介紹自定義LINQ Provider中兩個最重要的接口IQueryable和IQueryProvider。

IEnumerable<T>接口

在上一篇《打造自己的LINQ Provider(上):Expression Tree揭祕》一文的最後,我說到了這樣一句話:需要注意的是LINQ to Objects並不需要任何特定的LINQ Provider,因爲它並不翻譯爲表達式目錄樹,帶着這個問題,我們先來看下面這段代碼,查詢的結果query爲IEnumerable<String>類型:
static void Main(string[] args)
{
    List<String> myList = new List<String>() { "a", "ab", "cd", "bd" };
    IEnumerable<String> query = from s in myList
                where s.StartsWith("a")
                select s;
    foreach (String s in query)
    {
        Console.WriteLine(s);
    }
    Console.Read();
}
這裏將返回兩條結果,如下圖所示:
TerryLee_0170
這裏就有一個問題,爲什麼在LINQ to Objects中返回的是IEnumerable<T>類型的數據而不是IQueryable<T>呢?答案就在本文的開始,在LINQ to Objects中查詢表達式或者Lambda表達式並不翻譯爲表達式目錄樹,因爲LINQ to Objects查詢的都是實現了IEnmerable<T>接口的數據,所以查詢表達式或者Lambda表達式都可以直接轉換爲.NET代碼來執行,無需再經過轉換爲表達式目錄這一步,這也是LINQ to Objects比較特殊的地方,它不需要特定的LINQ Provider。我們可以看一下IEnumerable<T>接口的實現,它裏面並沒有Expression和Provider這樣的屬性,如下圖所示:
TerryLee_0171
至於LINQ to Objects中所有的標準查詢操作符都是通過擴展方法來實現的,它們在抽象類Enumerable中定義,如其中的Where擴展方法如下代碼所示:
public static class Enumerable
{
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate)
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        if (predicate == null)
        {
            throw Error.ArgumentNull("predicate");
        }
        return WhereIterator<TSource>(source, predicate);
    }
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source, 
        Func<TSource, int, bool> predicate)
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        if (predicate == null)
        {
            throw Error.ArgumentNull("predicate");
        }
        return WhereIterator<TSource>(source, predicate);
    }
}
注意到這裏方法的參數Func<TSource>系列委託,而非Expression<Func<TSource>>,在本文的後面,你將看到,IQueryable接口的數據,這些擴展方法的參數都是Expression<Func<TSource>>,關於它們的區別在上一篇文章我已經說過了。同樣還有一點需要說明的是,在IEnumerable<T>中提供了一組擴展方法AsQueryable(),可以用來把一個IEnumerable<T>類型的數據轉換爲IQueryable<T>類型,如下代碼所示:
static void Main(string[] args)
{
    var myList = new List<String>() 
                { "a", "ab", "cd", "bd" }.AsQueryable<String>();
    IQueryable<String> query = from s in myList
                where s.StartsWith("a")
                select s;
    foreach (String s in query)
    {
        Console.WriteLine(s);
    }
    Console.Read();
} 
運行這段代碼,雖然它的輸出結果與上面的示例完全相同,但它們查詢的機制卻完全不同:
TerryLee_0170

IQueryable<T>接口

在.NET中,IQueryable<T>繼承於IEnumerable<T>和IQueryable接口,如下圖所示:
TerryLee_0172
這裏有兩個很重要的屬性Expression和Provider,分別表示獲取與IQueryable 的實例關聯的表達式目錄樹和獲取與此數據源關聯的查詢提供程序,我們所有定義在查詢表達式中方法調用或者Lambda表達式都將由該Expression屬性表示,而最終會由Provider表示的提供程序翻譯爲它所對應的數據源的查詢語言,這個數據源可能是數據庫,XML文件或者是WebService等。該接口非常重要,在我們自定義LINQ Provider中必須要實現這個接口。同樣對於IQueryable的標準查詢操作都是由Queryable中的擴展方法來實現的,如下代碼所示:
public static class Queryable
{
    public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, 
            Expression<Func<TSource, bool>> predicate)
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        if (predicate == null)
        {
            throw Error.ArgumentNull("predicate");
        }
        return source.Provider.CreateQuery<TSource>(
            Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod())
            .MakeGenericMethod(new Type[] { typeof(TSource) }), 
            new Expression[] { source.Expression, Expression.Quote(predicate) }));
    }
    public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source,
        Expression<Func<TSource, int, bool>> predicate)
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        if (predicate == null)
        {
            throw Error.ArgumentNull("predicate");
        }
        return source.Provider.CreateQuery<TSource>(
            Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod())
            .MakeGenericMethod(new Type[] { typeof(TSource) }), 
            new Expression[] { source.Expression, Expression.Quote(predicate) }));
    }
}
最後還有一點,如果我們定義的查詢需要支持Orderby等操作,還必須實現IOrderedQueryable<T> 接口,它繼承自IQueryable<T>,如下圖所示:
TerryLee_0173 

IQueryProvider接口

在認識了IQueryable接口之後,我們再來看看在自定義LINQ Provider中另一個非常重要的接口IQueryProvider。它的定義如下圖所示:
TerryLee_0174
看到這裏兩組方法的參數,其實大家已經可以知道,Provider負責執行表達式目錄樹並返回結果。如果是LINQ to SQL的Provider,則它會負責把表達式目錄樹翻譯爲T-SQL語句並並傳遞給數據庫服務器,並返回最後的執行的結果;如果是一個Web Service的Provider,則它會負責翻譯表達式目錄樹並調用Web Service,最終返回結果。
這裏四個方法其實就兩個操作CreateQuery和Execute(分別有泛型和非泛型),CreateQuery方法用於構造一個 IQueryable<T> 對象,該對象可計算指定表達式目錄樹所表示的查詢,返回的結果是一個可枚舉的類型,;而Execute執行指定表達式目錄樹所表示的查詢,返回的結果是一個單一值。自定義一個最簡單的LINQ Provider,至少需要實現IQueryable<T>和IQueryProvider兩個接口,在下篇文章中,你將看到一個綜合的實例。

擴展LINQ的兩種方式

通過前面的講解,我們可以想到,對於LINQ的擴展有兩種方式,一是藉助於LINQ to Objects,如果我們所做的查詢直接在.NET代碼中執行,就可以實現IEnumerable<T>接口,而無須再去實現IQueryable並編寫自定義的LINQ Provider,如.NET中內置的List<T>等。如我們可以編寫一段簡單自定義代碼:
public class MyData<T> : IEnumerable<T>
                where T : class
{
    public IEnumerator<T> GetEnumerator()
    {
        return null;
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return null;
    }
    // 其它成員
}
第二種擴展LINQ的方式當然就是自定義LINQ Provider了,我們需要實現IQueryable<T>和IQueryProvider兩個接口,下面先給出一段簡單的示意代碼,在下一篇中我們將完整的來實現一個LINQ Provider。如下代碼所示:
public class QueryableData<TData> : IQueryable<TData>
{
    public QueryableData()
    {
        Provider = new TerryQueryProvider();
        Expression = Expression.Constant(this);
    }
    public QueryableData(TerryQueryProvider provider, 
        Expression expression)
    {
        if (provider == null)
        {
            throw new ArgumentNullException("provider");
        }
        if (expression == null)
        {
            throw new ArgumentNullException("expression");
        }
        if (!typeof(IQueryable<TData>).IsAssignableFrom(expression.Type))
        {
            throw new ArgumentOutOfRangeException("expression");
        }
        Provider = provider;
        Expression = expression;
    }
    public IQueryProvider Provider { get; private set; }
    public Expression Expression { get; private set; }
    public Type ElementType
    {
        get { return typeof(TData); }
    }
    public IEnumerator<TData> GetEnumerator()
    {
        return (Provider.Execute<IEnumerable<TData>>(Expression)).GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return (Provider.Execute<IEnumerable>(Expression)).GetEnumerator();
    }
}
public class TerryQueryProvider : IQueryProvider
{
    public IQueryable CreateQuery(Expression expression)
    {
        Type elementType = TypeSystem.GetElementType(expression.Type);
        try
        {
            return (IQueryable)Activator.CreateInstance(
                typeof(QueryableData<>).MakeGenericType(elementType),
                new object[] { this, expression });
        }
        catch
        {
            throw new Exception();
        }
    }
    public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
    {
        return new QueryableData<TResult>(this, expression);
    }
    public object Execute(Expression expression)
    {
        // ......
    }
    public TResult Execute<TResult>(Expression expression)
    {
        // ......
    }
}
上面這兩個接口都沒有完成,這裏只是示意性的代碼,如果實現了這兩個接口,我們就可以像下面這樣使用了(當然這樣的使用是沒有意義的,這裏只是爲了演示):
static void Main(string[] args)
{
    QueryableData<String> mydata = new QueryableData<String> { 
        "TerryLee",
        "Cnblogs",
        "Dingxue"
    };
    var result = from d in mydata
                 select d;
    foreach (String item in result)
    {
        Console.WriteLine(item);
    }
}
現在再來分析一下這個執行過程,首先是實例化QueryableData<String>,同時也會實例化TerryQueryProvider;當執行查詢表達式的時候,會調用TerryQueryProvider中的CreateQuery方法,來構造表達式目錄樹,此時查詢並不會被真正執行(即延遲加載),只有當我們調用GetEnumerator方法,上例中的foreach,此時會調用TerryQueryProvider中的Execute方法,此時查詢纔會被真正執行,如下圖所示:
TerryLee_0178 

總結

本文介紹了在自定義LINQ Provider中兩個最重要的接口IQueryable和IQueryProvider,希望對大家有所幫助,下一篇我我們將開發一個完整的自定義LINQ Provider。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章