找回密码
 立即注册
查看: 31728|回复: 99

C#经典面试题——深入理解IEnumerable和IQueryable两接口的区别

    [复制链接]
  • TA的每日心情
    慵懒
    2021-11-26 16:44
  • 签到天数: 488 天

    连续签到: 1 天

    [LV.9]妙领天机

    488

    主题

    3011

    回帖

    4611

    积分

    如雷贯耳

    积分
    4611
    发表于 2018-7-23 20:11:49 | 显示全部楼层 |阅读模式

    马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

      不管是现在正在从事.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();
      }

                                   
    登录/注册后可看大图
    楼主热帖
  • TA的每日心情
    难过
    2019-8-30 19:44
  • 签到天数: 456 天

    连续签到: 2 天

    [LV.9]妙领天机

    2

    主题

    3071

    回帖

    3072

    积分

    声名显赫

    积分
    3072
    发表于 2018-7-23 20:20:47 | 显示全部楼层
    我了个去,顶了
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2021-9-14 11:28
  • 签到天数: 483 天

    连续签到: 1 天

    [LV.9]妙领天机

    449

    主题

    3048

    回帖

    4379

    积分

    如雷贯耳

    积分
    4379
    发表于 2018-7-24 22:39:42 | 显示全部楼层
    无论是不是沙发都得回复下
    回复

    使用道具 举报

  • TA的每日心情
    难过
    2019-8-30 04:18
  • 签到天数: 476 天

    连续签到: 2 天

    [LV.9]妙领天机

    3

    主题

    3098

    回帖

    3106

    积分

    声名显赫

    积分
    3106
    发表于 2018-7-26 02:35:31 | 显示全部楼层
    锄禾日当午,发帖真辛苦。谁知坛中餐,帖帖皆辛苦!
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2019-8-30 11:35
  • 签到天数: 482 天

    连续签到: 6 天

    [LV.9]妙领天机

    10

    主题

    2981

    回帖

    3000

    积分

    声名显赫

    积分
    3000
    发表于 2018-7-26 14:50:36 | 显示全部楼层
    支持你哈...................................
    回复

    使用道具 举报

  • TA的每日心情
    难过
    2019-8-29 21:08
  • 签到天数: 481 天

    连续签到: 1 天

    [LV.9]妙领天机

    1

    主题

    3037

    回帖

    3039

    积分

    声名显赫

    积分
    3039
    发表于 2018-7-26 18:33:56 | 显示全部楼层
    我是个凑数的。。。
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    2019-8-30 11:30
  • 签到天数: 481 天

    连续签到: 1 天

    [LV.9]妙领天机

    0

    主题

    3064

    回帖

    3059

    积分

    声名显赫

    积分
    3059
    发表于 2018-7-28 06:38:04 | 显示全部楼层
    支持,楼下的跟上哈~
    回复

    使用道具 举报

  • TA的每日心情
    奋斗
    2019-8-30 17:39
  • 签到天数: 464 天

    连续签到: 3 天

    [LV.9]妙领天机

    8

    主题

    3049

    回帖

    3064

    积分

    声名显赫

    积分
    3064
    发表于 2018-7-29 02:12:14 | 显示全部楼层
    支持楼主,用户楼主,楼主英明呀!!!
    回复

    使用道具 举报

  • TA的每日心情
    奋斗
    2019-8-29 16:41
  • 签到天数: 465 天

    连续签到: 3 天

    [LV.9]妙领天机

    17

    主题

    3069

    回帖

    3099

    积分

    声名显赫

    积分
    3099
    发表于 2018-7-29 10:31:36 | 显示全部楼层
    不错 支持下
    回复

    使用道具 举报

  • TA的每日心情
    无聊
    2019-8-30 12:54
  • 签到天数: 477 天

    连续签到: 2 天

    [LV.9]妙领天机

    0

    主题

    3117

    回帖

    3117

    积分

    声名显赫

    积分
    3117
    发表于 2018-7-31 10:29:01 | 显示全部楼层
    非常好,顶一下
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|Archiver|手机版|小黑屋|任逍遥

    GMT+8, 2024-11-22 18:41 , Processed in 0.084343 second(s), 48 queries .

    Powered by 任逍遥 X3.5

    Copyright © 2001-2024, Rxiaoyao Cloud.

    快速回复 返回顶部 返回列表