1. 首页
  2. 文章列表
  3. C#经典面试题——深入理解IEnumerable和IQueryable两接口的区别

不管是现在正在从事.NET相关开发工作,还是现在正在学习.NET的小伙伴,对这两个接口类一定不陌生,也许也能很熟练的运用这两个接口对数据库或者集合进行各种复杂的操作,但是你们可能真的理解这两个类的用途或区别的我想,可能是少数了吧,毕竟面试时也是很多公司技术面试会考到的一个问题。

说到用法,可能大家都感觉这两个类似乎用法都一样啊,好像没什么区别,而由这个问题引出的各种系统优化问题,以及很多人吐槽EntityFramework效率低下的问题,那么,今天呢,就来专门针对这些问题做个深入研究咯。

无论是使用EntityFramework还是单纯的对List集合进行筛选排序分组聚合等操作,我们经常就是直接Where()、OrderBy()、GroupBy()、Sum()方法调用起走,管他返回是Enumerable还是Queryable,最后总是会得到我想要的结果,那么,它们究竟是如何定义的,都分别用来干什么的?又尤其是IQueryable,它和EntityFramework的延迟加载技术又有什么联系呢?

它们是什么?牵出来遛一遛

先来看一下这两个类的定义:

(1)Enumerable类,继承自IEnumerable<T>接口的集合进行扩展;

(2)Queryable类,继承自IQueryable<T>接口的集合进行扩展。

它们都在System.Linq命名空间下。

再继续深入学习之前,我们先来看一下EntityFramework的实体集DbSet<T>的实现:

懒得勤快的博客_全栈开发者_互联网分享精神

通过定义我们看到DbSet<T>同时实现了IEnumerable<T>和IQueryable<T>。

所以结合起来就是DbSet<T>通过实现IEnumerable<T>和IQueryable<T>在此基础上再有Enumerable和Queryable这两个静态类进行了很多方法的扩展。

懒得勤快的博客_全栈开发者_互联网分享精神

不过,这么多的扩展方法并不是我们都常用的,在EntityFramework中最常用的也就Where()、OrderBy()、GroupBy()、Sum()这些方法了。

同时我们观察其中的Where方法,可以看到第一个参数是实现了IEnumable接口的类,第二个参数是一个Func<T>委托类型。

我们继续拿出Queryable的方法定义来看看:

懒得勤快的博客_全栈开发者_互联网分享精神

观察Where方法,可以看到第一个参数是实现了IQueryable接口的类,第二个参数是一个Expression<Func<T>>的表达式树类型(不知道什么是表达式树的,可以参阅下这篇文章)。

Queryable和Enumerable里面的Where既然参数是不一样的,那么它们的用途肯定就是不一样的了,究竟有什么不一样?这种不一样又会造成什么结果?为什么IQueryable的方法都要把Func包在Expression里面?

其实我们反编译仔细观察IQueryable的话,实际上也继承了IEnumerable,所以这两个接口的方法,在很大程度上是一样的,那么,微软为什么要设计出两套扩展方法呢?

好,下面继续深入研究一下。

为了方便大家更直观的理解,接下来就用代码驱动了,透过现象看本质。

代码走起

我们先建一个控制台程序,然后把EntityFramework包引入,并自动生成一些实体和数据库上下文:

static void Main(string[] args)
{
    using (DataContext db = new DataContext())
    {
        var posts = db.Post.Where(p => p.Title.Contains("web前端"));
        foreach (Post p in posts)
        {
            Console.WriteLine(p.Title);
        }
    }
}

好,我们开始打断点调试程序,注意观察VS的诊断工具或者是你也可以使用SQL Server的Profile工具来跟踪SQL语句的执行;

懒得勤快的博客_全栈开发者_互联网分享精神

断点命中,但是诊断工具的时间里面什么也没有,

懒得勤快的博客_全栈开发者_互联网分享精神

我们F10走一步,好,出现了几个ADO.NET的事件,说明有SQL语句执行了,

懒得勤快的博客_全栈开发者_互联网分享精神

但是,为什么有6个ADO.NET事件,难道执行了6次,我们查看详细信息会发现,这些都是EF在做数据迁移的检查(CodeFirst),因为SQL语句里面From跟的是_Migration表,这是CodeFirst自动生成帮助EF做迁移用的,这里不做过多的解释,有兴趣的下来可以自己去详细研究下这张表里存的是些什么东西。

我们继续F10走一步,依然没执行SQL语句,(眼睛尖的孩子肯定已经看到我第三方调试器已经提示出SQL语句了),

懒得勤快的博客_全栈开发者_互联网分享精神

再走一步到in,依然没执行,诶,是不是出什么问题了呢?为什么没有查询语句执行呢?难道是VS诊断工具出问题了吗?

那就再走一步看看,好,终于有ADO.NET事件了

懒得勤快的博客_全栈开发者_互联网分享精神

恩,select…From Post。这就是EF帮我们生成的SQL语句了,很奇怪吧,这就是EF的延迟加载技术,这里面很重要的一部分就是通过IQueryable接口实现的。

讲过了Queryable类的Where方法,接下来我们再来看一下Enumable类的Where方法。

修改上面的代码如下所示:

static void Main(string[] args)
{
    using (DataContext db = new DataContext())
    {
        var posts = db.Post.AsEnumerable().Where(p => p.Title.Contains("web前端"));
        foreach (Post p in posts)
        {
            Console.WriteLine(p.Title);
        }
    }
}

懒得勤快的博客_全栈开发者_互联网分享精神

同样打断点开始跟踪,我们发现用IEnumerable进行筛选的时候,所生成的SQL语句是这样的:

懒得勤快的博客_全栈开发者_互联网分享精神

对比之前的发现,居然没有where条件语句了!

结论

所以通过上面的两个测试得出结论:

(1)所有对于IEnumerable的过滤、排序、分组、聚合等操作,都是在内存中进行的。也就是说把所有的数据不管用不用得到,都从数据库倒入内存中,只是在内存中进行过滤和排序操作,但性能很高,空间换时间,用于操作本地数据源。

(2)所有对于IQueryable的过滤、排序、分组、聚合等操作,只有在数据真正用到的时候才会到数据库中查询,以及只把需要的数据筛选到内存中。Linq to SQL引擎会把表达式树转化成相应的SQL在数据库中执行,这也是Linq的延迟加载核心思想所在,在很复杂的操作下可能比较慢了,时间换空间。

(3)操作本地数据源用IEnumerable,操作远程数据源用

那么最后的一个问题,IQueryable接口的特殊之处?

我们继续观察它的定义:

懒得勤快的博客_全栈开发者_互联网分享精神

它继承自Ienumerable,但很遗憾,这里面只有几个属性,这IQueryable和IEnumerable到底有什么不同?通过刚才的实例和Queryable的定义我们也看出了区别,所以答案就是:Expression会把查询表达式生成表达式树缓存起来,只有当真正需要用到的时候,才会由IQueryProvider解析表达式树,翻译成sql语句执行数据库查询操作。而Func是个委托,必须要先执行完才能进行下一个方法的调用。

更直白点的说,就是:IQueryable是负责生成SQL语句的,但并不马上执行;而IEnumerable是对任意类型的集合都能操作的,不限于是数据库还是一般的Array还是List。

所以,这两个类在使用上也不是想用那个就用哪个,所以那些说EntityFramework不行的,你真的理解这两个类么?!

只有真正的了解其原理,才能谈性能优化!

关于List<T>

最后,我们来看看List<T>这个类:

懒得勤快的博客_全栈开发者_互联网分享精神

List<T>也继承自IEnumerable,所以Where、OrderBy这些方法也能用咯,上代码:

static void Main(string[] args)
{
    List<string> list = new List<string>() { "aaaab", "caabb", "abcde", "uvwxyz", "qwertyuiop" };
    IEnumerable<string> where = list.Where(s => s.Contains("x"));
    IOrderedEnumerable<string> orderby = list.OrderBy(s => s);
    foreach (string s in @where)
    {
        Console.WriteLine(s);
    }
}

同时,List<T>它也是继承了IEnumerable接口,所以,它也不是延迟加载的,但支持延迟查询

static void Main(string[] args)
{
   List<string> list = new List<string>() { "aaaab", "caabb", "abcde", "uvwxyz", "qwertyuiop" };
   IEnumerable<string> result1 = list.Where(s => s.Contains("x"));
   var result2 = list.Where(s => s.Contains("x")).ToList();
   list[0] = "xxaab";
   Console.WriteLine("result1:");
   foreach (string s in result1)
   {
       Console.Write(s + "\t");
   }
   Console.WriteLine("\nresult2:");
   foreach (var s in result2)
   {
       Console.Write(s + "\t");
   }
   Console.ReadKey();
}

懒得勤快的博客_全栈开发者_互联网分享精神

分享到:

下载为Word文档

文章历史版本:

修改次数:4 次 查看历史版本

版权声明:

本文仅用于学习、研究和交流目的,欢迎非商业性质转载。本文链接:https://masuit.com/171

l  博主在此发文(包括但不限于汉字、拼音、拉丁字母)均为随意敲击键盘所出,用于检验本人电脑键盘录入、屏幕显示的机械、光电性能,并不代表本人局部或全部同意、支持或者反对观点。如需要详查请直接与键盘生产厂商法人代表联系。挖井挑水无水表,不会网购无快递。

l  文章内容部分来源于互联网,不代表本人的任何立场;涉及到的软件来源于互联网,仅供个人下载使用,请勿用于商业用途,版权归软件开发者所有,下载后请于24小时内删除,如有真实需要请支持正版!因下载本站任何资源造成的损失,全部责任由使用者本人承担!如果你是版权方,认为本文内容对您的权益有所侵犯,请联系博主,并参照侵删联系的说明提交相应的证明材料,待博主进行严格地审查和背景调查后,情况属实的将在三天内将本文删除或修正。

l  博主的文章没有高度、深度和广度,只是凑字数。由于博主的水平不高(其实是个菜B),不足和错误之处在所难免,希望大家能够批评指出。

l  博主是利用读书、参考、引用、抄袭、复制和粘贴等多种方式打造成自己的纯镀 24k 文章,请原谅博主成为一个无耻的文档搬运工!

l  博主只是一名普通的互联网从业者,不会帮你盗号,不懂破解开机密码,找不回你丢失的手机等,如有这样的想法我也帮你实现不了!

相关推荐:

AutoMapper 6.x起步 经典面试题之——如何自由转换两个没有继承关系的字段及类型相同的实体模型
C# vs Java:C# 五个不可替代的特性瞬间秒杀 Java 由double类型判等引发的一点小思考
用C#实现求有向图的最长路径和最短路径 五分钟重温C#委托,匿名方法,Lambda,泛型委托,表达式树
Autofac在.NET Core中的属性注入 浅谈MVC的Attribute路由,教你一步一步设计出漂亮的路由
谈一谈单例模式、静态类和线程内唯一对象有什么区别 深入理解C#中的IDisposable接口

评论区:

    还没有评论哦,赶紧来写评论吧