.NET中那些所谓的新语法之四:标准查询运算符与LINQ

开篇:在上一篇中,我们了解了预定义委托与Lambda表达式等所谓的新语法,这一篇我们继续征程,看看标准查询运算符和LINQ。标准查询运算符是定义在System.Linq.Enumerable类中的50多个为IEnumerable<T>准备的扩展方法,而LINQ则是一种类似于SQL风格的查询表达式,它们可以大大方便我们的日常开发工作。因此,需要我们予以关注起来!

/* 新语法索引 */

9.标准查询运算符 Standard Query Operator
10.LINQ查询表达式

一、扩展方法哪家强?标准查询运算符

  标准查询运算符提供了包括筛选、投影、聚合、排序等功能在内的查询功能,其本质是定义在System.Linq.Enumerable类中的50多个为IEnumerable<T>准备的扩展方法

  从上图可以看出,在Enumerable类中提供了很多的扩展方法,这里我们选择其中几个最常用的方法来作一点介绍,使我们能更好地利用它们。首先,我们需要一点数据来进行演示:

    public class Person
    {
        public int ID { get; set; }

        public string Name { get; set; }

        public int Age { get; set; }

        public bool Gender { get; set; }

        public override string ToString()
        {
            return string.Format("{0}-{1}-{2}-{3}", ID, Name, Age, 
                Gender == true ? "" : "");
        }
    }

    public class LitePerson
    {
        public string Name { get; set; }

        public override string ToString()
        {
            return Name;
        }
    }

    public class Children
    {
        public int ChildID { get; set; }
        public int ParentID { get; set; }
        public string ChildName { get; set; }

        public override string ToString()
        {
            return string.Format("{0}-{1}-{2}", ChildID, ChildName, ParentID);
        }
    }

        static List<Person> GetPersonList()
        {
            List<Person> personList = new List<Person>()
            {
                new Person(){ID=1,Name="Edison Chou",Age=25,Gender=true},
                new Person(){ID=2,Name="Edwin Chan",Age=20,Gender=true},
                new Person(){ID=3,Name="Jackie Chan",Age=40,Gender=true},
                new Person(){ID=4,Name="Andy Lau",Age=55,Gender=true},
                new Person(){ID=5,Name="Kelly Chan",Age=45,Gender=false}
            };
            return personList;
        }

        static List<Children> GetChildrenList()
        {
            List<Children> childrenList = new List<Children>()
            {
                new Children(){ChildID=1,ParentID=1,ChildName="Lucas"},
                new Children(){ChildID=2,ParentID=1,ChildName="Louise"},
                new Children(){ChildID=3,ParentID=3,ChildName="Edward"},
                new Children(){ChildID=4,ParentID=4,ChildName="Kevin"},
                new Children(){ChildID=5,ParentID=5,ChildName="Mike"}
            };
            return childrenList;
        }

        static List<Person> GetMorePersonList()
        {
            List<Person> personList = new List<Person>()
            {
                new Person(){ID=1,Name="爱迪生",Age=100,Gender=true},
                new Person(){ID=2,Name="瓦特",Age=120,Gender=true},
                new Person(){ID=3,Name="牛顿",Age =150,Gender=true},
                new Person(){ID=4,Name="图灵",Age=145,Gender=true},
                new Person(){ID=5,Name="香农",Age=120,Gender=true},
                new Person(){ID=6,Name="居里夫人",Age=115,Gender=false},
                new Person(){ID=6,Name="居里夫人2",Age=115,Gender=false},
                new Person(){ID=7,Name="居里夫人3",Age=115,Gender=false},
                new Person(){ID=8,Name="居里夫人4",Age=115,Gender=false},
                new Person(){ID=9,Name="居里夫人5",Age=115,Gender=false},
                new Person(){ID=10,Name="居里夫人6",Age=115,Gender=false},
                new Person(){ID=11,Name="居里夫人7",Age=115,Gender=false},
                new Person(){ID=12,Name="居里夫人8",Age=115,Gender=false},
                new Person(){ID=13,Name="居里夫人9",Age=115,Gender=false},
                new Person(){ID=14,Name="居里夫人10",Age=115,Gender=false},
                new Person(){ID=15,Name="居里夫人11",Age=115,Gender=false},
                new Person(){ID=16,Name="居里夫人12",Age=115,Gender=false},
                new Person(){ID=17,Name="居里夫人13",Age=115,Gender=false},
                new Person(){ID=18,Name="居里夫人14",Age=115,Gender=false}
            };

            return personList;
        }
View Code

1.1 筛选高手Where方法

  Where方法提供了我们对于一个集合的筛选功能,但需要提供一个带bool返回值的“筛选器”(匿名方法、委托、Lambda表达式均可),从而表明集合中某个元素是否应该被返回。这里,我们以上面的数据为例,筛选出集合中所有性别为男,年龄大于20岁的子集合,借助Where方法实现如下:

        static void SQOWhereDemo()
        {
            List<Person> personList = GetPersonList();

            List<Person> maleList = personList.Where(p =>
                p.Gender == true && p.Age > 20).ToList();
            maleList.ForEach(m => Console.WriteLine(m.ToString()));
        }

  (1)运行结果如下图所示:

  (2)由本系列文章的第二篇可知,扩展方法的本质是在运行时调用扩展类的静态方法,而我们写的Lambda表达式在编译时又会被转为匿名方法(准确地说应该是预定义泛型委托实例)作为方法参数传入扩展方法中,最后调用执行该扩展方法生成一个新的List集合返回。

1.2 投影大牛Select方法

  Select方法可以查询投射,返回新对象集合。这里,假设我们先筛选出所有男性集合,再根据男性集合中所有项的姓名生成子集合(这是一个不同于原类型的类型),就可以借助Select方法来实现。

        static void SQOSelectDemo()
        {
            List<Person> personList = GetPersonList();

            List<LitePerson> liteList = personList.Where(p =>
                p.Gender == true).Select(
                p => new LitePerson() { Name = p.Name }).ToList();
            liteList.ForEach(p => Console.WriteLine(p.ToString()));
        }

  (1)运行结果如下图所示:

  (2)这里也可以采用匿名类,可以省去事先声明LitePerson类的步凑,但需要配合var使用:

var annoyList = personList.Where(p =>
    p.Gender == true).Select(
    p => new { Name = p.Name }).ToList();

  (3)这里因为实现LitePerson类重写了ToString()方法,所以这里直接调用了ToString()方法。

1.3 排序小生OrderBy方法

  说到排序,我们马上想起了SQL中的order by语句,而标准查询运算符中也为我们提供了OrderBy这个方法,值得一提的就是我们可以进行多条件的排序,因为OrderBy方法返回的仍然是一个IEnumerable<T>的类型,仍然可以继续使用扩展方法。但要注意的是,第二次应该使用ThenBy方法。

        static void SQOOrderByDemo()
        {
            List<Person> personList = GetPersonList();
            // 单条件升序排序
            Console.WriteLine("Order by Age ascending:");
            List<Person> orderedList = personList.OrderBy(p => p.Age).ToList();
            orderedList.ForEach(p => Console.WriteLine(p.ToString()));
            // 单条件降序排序
            Console.WriteLine("Order by Age descending:");
            orderedList = personList.OrderByDescending(p => p.Age).ToList();
            orderedList.ForEach(p => Console.WriteLine(p.ToString()));
            // 多条件综合排序
            Console.WriteLine("Order by Age ascending and ID descending:");
            orderedList = personList.OrderBy(p => p.Age)
                .ThenByDescending(p => p.ID).ToList();
            orderedList.ForEach(p => Console.WriteLine(p.ToString()));
        }

  运行结果如下图所示:

1.4 连接道士Join方法

  在数据库中,我们对两个表或多个表进行连接查询时往往会用到join语句,然后指定两个表之间的关联关系(例如: a.bid = b.aid)。在标准查询运算符中,细心的.NET基类库也为我们提供了Join方法。现在,假设我们有两个类:Person和Children,其中每个Children对象都有一个ParentID,对应Person对象的ID,现需要打印出所有Person和Children的信息,可以借助Join方法来实现。

        static void SQOJoinDemo()
        {
            List<Person> personList = GetPersonList();
            List<Children> childrenList = GetChildrenList();

            // 连接查询
            var joinedList = personList.Join(childrenList,
                p => p.ID, c => c.ParentID, (p, c) => new
                {
                    ParentID = p.ID,
                    ChildID = c.ChildID,
                    ParentName = p.Name,
                    ChildName = c.ChildName
                }).ToList();
            joinedList.ForEach(c => Console.WriteLine(c.ToString()));
        }

  运行结果如下图所示:

1.5 分组老师GroupBy方法

  在数据库中,我们要对查询结果进行分组会用到 group by 语句,在标准查询运算符中,我们也有对应的GroupBy方法。这里,假设我们对Person数据集按照性别进行分类,该怎么来写代码呢?

        static void SQOGroupByDemo()
        {
            List<Person> personList = GetPersonList();

            IEnumerable<IGrouping<bool, Person>> groups =
                personList.GroupBy(p => p.Gender);
            IList<IGrouping<bool, Person>> groupList = groups.ToList();

            foreach (IGrouping<bool, Person> group in groupList)
            {
                Console.WriteLine("Group:{0}", group.Key ? "" : "");
                foreach (Person p in group)
                {
                    Console.WriteLine(p.ToString());
                }
            }
        }

  (1)这里需要注意的是:通过GroupBy方法后返回的是一个IEnumerable<IGrouping<TKey, TSource>>类型,其中TKey是分组依据的类型,这里是根据Gender来分组的,而Gender又是bool类型,所以TKey这里为bool类型。TSource则是分组之后各个元素的类型,这里是将List<Person>集合进行分组,因此分完组后每个元素都存储的是Person类型,所以TSource这里为Person类型,Do you understand now?

  (2)运行结果如下图所示:

  (3)可能有人会说我咋记得住GroupBy返回的那个类型,太长了,我也不想记。怎么办呢?不怕,我们可以使用var关键字嘛:

            var annoyGroups = personList.GroupBy(p => p.Name).ToList();
            foreach (var group in annoyGroups)
            {
                Console.WriteLine("Group:{0}", group.Key);
                foreach (var p in group)
                {
                    Console.WriteLine(p.ToString());
                }
            }

1.6 分页实战Skip与Take方法

  相信很多人都使用过标准查询运算符进行分页操作,这里我们再次来看看如何借助Skip与Take方法来实现分页操作。还是以PersonList集合为例,假如页面上的表格每页显示5条数据,该怎么来写代码呢?

        static void SQOPagedDemo()
        {
            // 这里假设每页5行数据
            // 第一页
            Console.WriteLine("First Page:");
            var firstPageData = GetPagedListByIndex(1, 5);
            firstPageData.ForEach(d => Console.WriteLine(d.ToString()));
            // 第二页
            Console.WriteLine("Second Page:");
            var secondPageData = GetPagedListByIndex(2, 5);
            secondPageData.ForEach(d => Console.WriteLine(d.ToString()));
            // 第三页
            Console.WriteLine("Third Page:");
            var thirdPageData = GetPagedListByIndex(3, 5);
            thirdPageData.ForEach(d => Console.WriteLine(d.ToString()));
        }

        static List<Person> GetPagedListByIndex(int pageIndex, int pageSize)
        {
            List<Person> dataList = GetMorePersonList();
            return dataList.Skip((pageIndex - 1) * pageSize)
                .Take(pageSize).ToList();
        }

  运行结果如下图所示:

1.7 浅谈延迟加载与即时加载

  (1)延迟加载(Lazy Loading):只有在我们需要数据的时候才去数据库读取加载它。

  在标准查询运算符中,Where方法就是一个典型的延迟加载案例。在实际的开发中,我们往往会使用一些ORM框架例如EF去操作数据库,Where方法的使用则是每次调用都只是在后续生成SQL语句时增加一个查询条件,EF无法确定本次查询是否已经添加结束,所以没有办法木有办法在每个Where方法执行的时候确定最终的SQL语句,只能返回一个DbQuery对象,当使用到这个DbQuery对象的时候,才会根据所有条件生成最终的SQL语句去查询数据库。

    var searchResult = personList.Where(p =>
          p.Gender == false).Where(p => p.Age > 20)
          .Where(p=>p.Name.Contains("奶茶"));

  (2)即时加载(Eager Loading):加载数据时就把该对象相关联的其它表的数据一起加载到内存对象中去。

  在标准查询运算符中,FindAll方法就是一个典型的即时加载案例。与延迟加载相对应,在开发中如果使用FindAll方法,EF会根据方法中的条件自动生成SQL语句,然后立即与数据库进行交互获取查询结果,并加载到内存中去。

        var searchResult = personList.FindAll(p=>p.Gender == false
                && p.Name.Contains("奶茶"));

二、查询方式谁更快?LINQ

2.1 初识LINQ:类似SQL风格的代码

  LINQ又称语言集成查询,它是C# 3.0的新语法。在更多的人看来,它是一种方便的查询表达式,或者说是和SQL风格接近的代码

    var maleList = from p in personList
                   where p.Gender == true
                   select p;

  (1)LINQ表达式以"from"开始,以"select 或 group by子句"结尾;

  (2)LINQ表达式的输出是一个 IEnumerable<T> 或 IQueryable<T> 集合;(注:T 的类型 由 select 或 group by 推断出来)

2.2 LINQ使用:实现除Skip和Take外的标准查询运算符的功能

  (1)基本条件查询:

            List<Person> personList = GetPersonList();
            List<Children> childList = GetChildrenList();
            // 基本条件查询
            Console.WriteLine("Basic Query:");
            var maleList = from p in personList
                           where p.Gender == true
                           select p;
            maleList.ToList().ForEach(m =>
                Console.WriteLine(m.ToString()));
View Code

  (2)排序条件查询:

            // 排序条件查询
            Console.WriteLine("Order Query:");
            var orderedList = from p in personList
                              orderby p.Age descending
                              orderby p.Name ascending
                              select p;
            orderedList.ToList().ForEach(m =>
                Console.WriteLine(m.ToString()));
View Code

  (3)连接查询:

            // Join连接查询
            Console.WriteLine("Join Query:");
            var joinedList = from p in personList
                             join c in childList
                             on p.ID equals c.ParentID
                             select new
                             {
                                 Person = p,
                                 Child = c
                             };
            foreach (var item in joinedList)
            {
                Console.WriteLine(item.ToString());
            }
View Code

  (4)分组查询:

            // 分组条件查询
            Console.WriteLine("Group Query:");
            var groupList = from p in personList
                            group p by p.Gender;
            foreach (var group in groupList)
            {
                Console.WriteLine("Group:{0}", 
                    group.Key? "":"");
                foreach(var item in group)
                {
                    Console.WriteLine(item.ToString());
                }
            }
View Code

  运行结果请参考上一节标准查询运算符中相关的运行结果,或下载附件运行查看,这里不再贴图。

2.3 LINQ本质:生成对应的标准查询运算符

  作为一个细心的.Net码农,我们不由得对LINQ表达式为我们做了哪些工作而好奇?于是,我们又想起了我们的“滑板鞋”—Reflector或ILSpy,去看看编译器为我们做了什么事!

  (1)以上述的基本条件查询代码为例,我们看到原来编译器将LINQ生成了对应的标准查询运算符,即Where扩展方法:

  (2)再来看看排序条件查询的代码,也是生成了对应的标准查询运算符,即OrderBy扩展方法:

  (3)总结:LINQ编译后会生成对应的标准查询运算符(查询->Where,排序->OrderBy,连接->Join,分组->GroupBy),所以LINQ表达式其实就是类似于SQL风格的一种更加友好语法糖而已。其本质还是扩展方法、泛型委托等“旧酒”,被一个“新瓶子”所包装了起来,就变得高大上了。

系列总结

  转眼之间,四篇文章的介绍就到此结束了,其实本系列介绍的都是不算新语法,其实也可以说成是老语法了。说它们新,只不过是相对于.NET老版本而言,而且平时开发中大家有可能没有注意到的一些细节,本系列做了一个简单的介绍。这几天看到很多园子里的童鞋开始关注C# 6.0的新特性了,粗略看了看,语法糖居多,相信经过了这一系列的探秘,对于新的语法糖,我们可以站在一个比较高的高度去看待它们。最后,谢谢各位园友的浏览,以及给我的一些鼓励,再次感谢!

参考文章

  (1)MSDN,《标准查询运算符概述》:http://msdn.microsoft.com/zh-cn/library/bb397896.aspx

  (2)MSDN,《LINQ(语言继承查询)》:http://msdn.microsoft.com/library/bb397926.aspx

  (3)jake强,《为提高EF性能需要注意哪些事情?》:http://www.cnblogs.com/jake1/archive/2013/04/25/3043664.html

附件下载

  NewGrammerDemo-v1.3:http://pan.baidu.com/s/1pJnyKtX

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。