打造自己的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。

相关文章:打造自己的LINQ Provider(上):Expression Tree揭秘

作者:TerryLee
出处:http://terrylee.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2008-08-25 23:59 TerryLee 阅读(6832) 评论(45)  编辑 收藏 网摘 所属分类: [06]  LINQ之美

  回复  引用    
#1楼2008-08-25 23:50 | Taker[未注册用户]
好文!
  回复  引用  查看    
#2楼2008-08-26 07:35 | 真见      
Linq 这个单词挺好看的。。。
  回复  引用  查看    
#3楼2008-08-26 09:05 | Mahon      
德智体美劳全面发展
  回复  引用  查看    
#4楼2008-08-26 09:24 | 狼Robot      
文章好长,晚上再来慢慢看.
  回复  引用  查看    
#5楼[楼主]2008-08-26 09:54 | TerryLee      
@Taker
谢谢:)

  回复  引用  查看    
#6楼[楼主]2008-08-26 09:55 | TerryLee      
@真见
只要单词好看没什么用,最重要的是它本身要好用:)

  回复  引用  查看    
#7楼[楼主]2008-08-26 09:55 | TerryLee      
@jannock
:-)

  回复  引用  查看    
#8楼[楼主]2008-08-26 09:56 | TerryLee      
@Mahon
这……

  回复  引用  查看    
#9楼[楼主]2008-08-26 09:56 | TerryLee      
@狼Robot
文章是有点长了,呵呵……

  回复  引用    
#10楼2008-08-26 10:53 | tsoukw[未注册用户]
不錯﹐有深度
  回复  引用  查看    
#11楼[楼主]2008-08-26 11:00 | TerryLee      
@tsoukw
多谢支持,这篇文章写的有点语无伦次:)

  回复  引用  查看    
#12楼2008-08-26 11:35 | BigRain      
好文章。有深度!晚上回去好好看看。
  回复  引用  查看    
#13楼2008-08-27 08:28 | 丁学      
发现TerryLee的文章从来不缺乏顶者~~~~~~~~`
  回复  引用  查看    
#14楼2008-08-27 08:32 | 丁学      
另外,建议CSS中关于.code的定义
.code{
border:dashed 1px #E0E0E0;padding:10px;background:#FFFEEE;
}
添加一项 line-height:1em; 如果嫌挤就用 1.2em;
你这个行间距太大了,如果是正文还好,代码写成这样,影响视觉连贯性

  回复  引用  查看    
#15楼[楼主]2008-08-27 09:43 | TerryLee      
@BigRain
谢了:)

  回复  引用  查看    
#16楼[楼主]2008-08-27 09:44 | TerryLee      
@丁学
多谢丁丁,已经加上了,之前一直没注意,呵呵:)

  回复  引用  查看    
#17楼2008-08-27 10:54 | Ivony...      
有些断言不太合适……

比如说:如果我们定义的查询需要支持Orderby等操作,还必须实现IOrderedQueryable 接口,它继承自IQueryable


  回复  引用  查看    
#18楼[楼主]2008-08-27 11:17 | TerryLee      
@Ivony...
就拿你举的这个例子来说,哪儿不合适呢?请指教……

  回复  引用  查看    
#19楼2008-08-27 21:46 | 香 妃      
思路清晰,期待下篇
  回复  引用  查看    
#20楼[楼主]2008-08-28 00:42 | TerryLee      
@香 妃
谢谢支持,我尽量快点完成下一篇:)

  回复  引用    
#21楼2008-08-28 09:38 | kangnoz[未注册用户]
(下)什么时候出啊,期待中。。。
  回复  引用  查看    
#22楼[楼主]2008-08-28 09:47 | TerryLee      
@kangnoz
一如既往,现在不能保证具体的发布时间,但是我一定会尽快完整:)

  回复  引用  查看    
#23楼2008-08-28 11:40 | Ivony...      
--引用--------------------------------------------------
TerryLee: @Ivony...
<br>就拿你举的这个例子来说,哪儿不合适呢?请指教……
--------------------------------------------------------

摘自Queryable类。

public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
if (keySelector == null)
{
throw Error.ArgumentNull("keySelector");
}
return (IOrderedQueryable<TSource>) source.Provider.CreateQuery<TSource>(Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(new Type[] { typeof(TSource), typeof(TKey) }), new Expression[] { source.Expression, Expression.Quote(keySelector) }));
}


所以,IOrderedQueryable<T>其实是对IQueryable<T>排序后的结果。
同样的IOrderedEnumerable<T>也是IEnumerable<T>排序后的结果。

在我的这篇文章中谈到了这一点:
http://www.cnblogs.com/Ivony/archive/2008/08/18/1270555.html" target="_new">http://www.cnblogs.com/Ivony/archive/2008/08/18/1270555.html

不过现在这篇文章正在重写。。。

  回复  引用  查看    
#24楼[楼主]2008-08-28 12:55 | TerryLee      
@Ivony...
这个我知道,IOrderedQueryable<T>和IOrderedEnumerable<T>表示经过排序的序列,如果自定义的查询需要支持排序操作,就需要实现IOrderedQueryable<T>接口了。

  回复  引用  查看    
#25楼2008-08-28 13:36 | Ivony...      
可不可以排序完全取决于你的IQueryProvider是否能够CreateQuery<TSource>( 一个对OrderBy方法调用的Expression )

但你排了序之后的结果必须放在IOrderedQueryable<T>中,用来告诉别人这个IQueryable<T>是有序的。

由于IOrderedQueryable<T>与IQueryable<T>是完全等同的,所以不存在实现的问题。


但是从语义上来说,IQueryable<T>不确定你每次GetEnumerator的时候获得的数据序列的顺序都是一样的,而IOrderedQueryable<T>却是确定的。

如果数据是以列表的形式存放在内存中,那么每次GetEnumerator的时候获得数据序列不一样的可能性是不存在的,但是如果数据不是在内存中甚至不是以列表的形式存放,那么就有可能了……

  回复  引用  查看    
#26楼2008-08-29 09:24 | jillzhang      
你这文章,得好好看看
  回复  引用  查看    
#27楼[楼主]2008-08-29 10:02 | TerryLee      
@Ivony...
非常感谢你的讨论。

其实你的解释跟我上面的说法并没有什么矛盾的地方,如果不实现IOrderedQueryable<T>,即便在CreateQuery<TSource>中排序了,但是排序结果用IQueryable<T>来表示,不就没有任何意义了嘛,总结起来一句话,如果自定义的查询要支持排序,需要用到IOrderedQueryable<T>接口,又回到开始了,呵呵

  回复  引用  查看    
#28楼[楼主]2008-08-29 10:02 | TerryLee      
@jillzhang
老张也来了啊,呵呵:)

  回复  引用  查看    
#29楼2008-11-05 21:04 | w i n s o n      
期待下集啊。。。。。
  回复  引用  查看    
#30楼[楼主]2008-11-06 20:23 | TerryLee      
@w i n s o n
这个系列的最后一篇,比较难产,呵呵,我需要写一个完整的LINQ Provider,目前正在进行中:)

  回复  引用  查看    
#31楼2008-11-23 12:35 | 孙孟      
--引用--------------------------------------------------
TerryLee: @w i n s o n
这个系列的最后一篇,比较难产,呵呵,我需要写一个完整的LINQ Provider,目前正在进行中:)
--------------------------------------------------------
加油加油 期待最后一篇

  回复  引用  查看    
#32楼[楼主]2008-11-24 09:57 | TerryLee      
@孙孟
:)

  回复  引用    
#33楼2008-12-16 14:56 | 方兵[未注册用户]
真的是好文章,发觉李老师总不会落后于当今流行技术。
  回复  引用  查看    
#34楼[楼主]2008-12-17 11:28 | TerryLee      
@方兵
谢谢支持,不学习就要落后啊:)

  回复  引用    
#35楼2008-12-19 10:48 | peak2007[未注册用户]
李老师:想问一下关于扩展方法(Extension methods),如:public static class Extensions{} 这是一种什么形式?
  回复  引用  查看    
#36楼[楼主]2008-12-21 11:21 | TerryLee      
@peak2007
啥意思?完全没有看懂想问什么?
public static class Extensions{} 就是一个静态类啊

  回复  引用    
#37楼2008-12-21 21:49 | peak2007[未注册用户]
李老师:我是在C#语言规范3.0里找的
""""10.6.9 扩展方法
当方法的第一个形参包含 this 修饰符时,称该方法为扩展方法 (extension method)。只能在非泛型、非嵌套静态类中声明扩展方法。扩展方法的第一个形参不能带有 this 之外的其他修饰符,而且形参类型不能是
指针类型。
下面是一个声明两个扩展方法的静态类的示例:
public static class Extensions
{
public static int ToInt32(this string s) {
return Int32.Parse(s);
}
public static T[] Slice<T>(this T[] source, int index, int count) {
if (index < 0 || count < 0 || source.Length index < count)
throw new ArgumentException();
T[] result = new T[count];
Array.Copy(source, index, result, 0, count);
return result;
}
}
扩展方法是常规静态方法。另外,如果它的包容静态类在范围之内,则可以使用实例方法调用语法(第 7.5.5.2
节)来调用扩展方法,同时将接收器表达式用作第一个实参。
下面的程序使用上面声明的扩展方法:
static class Program
{
static void Main() {
string[] strings = { "1", "22", "333", "4444
foreach (string s in strings.Slice(1, 2)) {
Console.WriteLine(s.ToInt32());
}
}
} """"

就是我可以自己写扩展方法,然后在主程序入口里调用自己扩展的方法.
我是想利用这一点来扩展对空间数据类型支持,以及自己写空间操作算子.
这样行吗?

  回复  引用  查看    
#38楼[楼主]2008-12-23 10:22 | TerryLee      
@peak2007
嗯,没有问题,扩展方法定义之后,可以在你的程序中任何地方调用,园子里有好几篇这样的文章,可以查找一下:)

  回复  引用    
#39楼2008-12-26 10:26 | peak2007[未注册用户]
李老师:好!
有一个问题如下:
string sentence = "This is a simple LINQ Demo!";
//在空格处分割
string[] words = sentence.Split(' ');
var query =words.GroupBy(w => w.Length, w => w.ToUpper()).Select(g => new { Length = g.Key, Words = g }).
OrderBy(o=>o.Length);(就是这一部分Lambda表达式,像.GroupBy在VS2008里没有智能感知显示?)

  回复  引用    
#40楼2008-12-26 14:49 | peak2007[未注册用户]
李老师:好!
有一个问题如下:
string sentence = "This is a simple LINQ Demo!";
//在空格处分割
string[] words = sentence.Split(' ');
var query =words.GroupBy(w => w.Length, w => w.ToUpper()).Select(g => new { Length = g.Key, Words = g }).
OrderBy(o=>o.Length);(就是这一部分Lambda表达式,像.GroupBy在VS2008里没有智能感知显示?)


  回复  引用  查看    
#41楼[楼主]2009-01-04 11:31 | TerryLee      
@peak2007
不可能吧,都应该有智能提示的。

  回复  引用  查看    
#42楼2009-01-14 21:04 | 阿不      
下篇呢?
  回复  引用    
#43楼2009-02-06 14:00 | 默蛇[未注册用户]
好大的一个坑
  回复  引用  查看    
#44楼2009-05-07 13:57 | 吾跃乾坤      
写得真好
请教个问题:

我用linq to entities 生成的实体,为什么没有外键字段呢

只要建了关系的字段都不生成属性

  回复  引用  查看    
#45楼2009-07-02 11:57 | 张亚      
下篇呢?
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1276200




相关文章:

相关链接: