首页 > 技术文章 > 进阶系列(9)——linq

wyh19941210 2017-12-27 22:43 原文

一、揭开linq的神秘面纱
(一)概述
  LINQ的全称是Language Integrated Query,中文译成“语言集成查询”。LINQ作为一种查询技术,首先要解决数据源的封装,大致使用了三大组件来实现这个封装,分别是LINQ to Object、LINQ to ADO.NET、LINQ to XML。它们和.NET语言的关系如下

  要使用LINQ来编程,首先要学习使用LINQ的子句以及由查询语法构成的查询表达式。C#3.0和VB9开始将这种查询语法引入到了编程语言,并新增了一系列的关键字。但对于CLR本身来说,它并不了解查询语法,它能理解的是由编程语言的编译器将这种查询语法转换成的方法。这些方法叫“标准查询运算符”,它们具有类似这样的名—Where、Select、GroupBy、Join。下面就以C#为例,从编程语言的层面来具体介绍这些查询语法(注意VB9也支持这种查询语法)。

LINQ的查询由3基本部分组成:获取数据源,创建查询,执行查询

     // 1,获取数据源
            List<int> numbers = new List<int>() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

            // 2,创建查询
            var numQuery = from num in numbers
                           where num % 2 == 0
                           select num;
            // 3,执行查询
            foreach (var num in numQuery)
            {
                Console.WriteLine("{0,1}", num);
            }

下图显示了完整的查询操作。在 LINQ 中,查询的执行与查询本身截然不同;换句话说,如果只是创建查询变量,则不会检索任何数据。

 

如上例所示,Linq的数据源要求必须实现IEnumerable或IEnumerable<T>接口,数组隐式支持这个接口。numQuery叫做查询变量,它存储了一个查询表达式。注意,声明查询变量并不会执行查询,真正的执行查询延迟到了foreach语句中。

linq的机制用到的详细知识点请参考:https://www.cnblogs.com/liulun/archive/2013/02/26/2909985.html

二、linq常用方法小试牛刀

1. from子句

    创建一个LINQ表达式必须要以from子句开头。

1.1 单个from子句

            string[] values = { "中国", "日本", "美国", "菲律宾", "越南" };

            //查询包含“国”的字符串
            var valueQuery = from v in values
                             where v.IndexOf("") > 0
                             select v;

            foreach (var v in valueQuery)
            {
                Console.WriteLine("{0,1}", v);
            }

在这个LINQ表达式的from子句中,v叫做范围变量,values是数据源。v的作用域存在于当前的LINQ表达式,表达式以外不能访问这个变量。where用来筛选元素,select用于输出元素。这里的范围变量v,和foreach语句中得隐式变量v都可以由编译器推断出其类型。
运行的结果如下:

中国
美国

使用LINQ查询List<T>集合

        public class CustomerInfo
        {
            public string Name { get; set; }
            public int Age { get; set; }
            public string Tel { get; set; }
        }
        private void formExpDemo2()
        {
            //这里用了,对象和集合初始化器
            List<CustomerInfo> customers = new List<CustomerInfo> { 
                                           new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                           new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                           new CustomerInfo{ Name="诸葛菲菲", Age=23, Tel ="1380524****"}
                                           };
            //查询年龄大于20的客户,注意这里的范围变量用了显示类型CustomerInfo
            var query = from CustomerInfo ci in customers
                        where ci.Age > 20
                        select ci;
            
            foreach (CustomerInfo ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }
        }

结果:

姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:诸葛菲菲 年龄:23 电话:1380524****

1.2 复合from子句

    在查询数据源中,元素的属性是一个集合时,可以使用复合from子句对这个属性集合查询。比如,一个客户,可能有多个电话。

        public class CustomerInfo
        {
            public string Name { get; set; }
            public int Age { get; set; }
            public List<string> TelTable { get; set; }
        }
        private void formExpDemo()
        {
            List<CustomerInfo> customers = new List<CustomerInfo> { 
                                           new CustomerInfo{ Name="欧阳晓晓", Age=35, TelTable=new List<string>{"1330708****","1330709****"}},
                                           new CustomerInfo{ Name="上官飘飘", Age=17, TelTable=new List<string>{"1592842****","1592843****"}},
                                           new CustomerInfo{ Name="诸葛菲菲", Age=23, TelTable=new List<string>{"1380524****","1380525****"}}
                                           };
            //查询包含电话号码1592842****的客户
            var query = from CustomerInfo ci in customers
                        from tel in ci.TelTable
                        where tel.IndexOf("1592842****") > -1
                        select ci;

            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1}", ci.Name, ci.Age);
                foreach (var tel in ci.TelTable)
                {
                    Console.WriteLine("          电话:{0}", tel);
                }
            }
        }

结果:

姓名:上官飘飘 年龄:17
          电话:1592842****
          电话:1592843****

1.3 多个from子句

        多个from子句查询和复合from子句从字面上看似乎一样,其实是不同的操作。复合from子句查询的是单个数据源中的子元素的集合,而多个from子句,是载入多个数据源进行查询。

        private void formExpDemo()
        {
            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="诸葛菲菲", Age=23, Tel ="1380524****"}
                                                   };
            List<CustomerInfo> clist2 = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="令狐冲", Age=25, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="东方不败", Age=35, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="任盈盈", Age=23, Tel ="1380524****"}
                                                   };

            //在clist中查找Age大于20的客户,
            //在clist2中查找Age小于30的客户
            var query = from customer in clist
                        where customer.Age > 20
                        from customer2 in clist2
                        where customer2.Age < 30
                        select new { customer, customer2 };

            foreach (var ci in query)
            {
                Console.WriteLine("{0} {1}", ci.customer.Name,ci.customer2.Name);
            }
        }

在select语句中,我们用了匿名类型来存储筛选出的元素,这样得到的完全是一个交叉联接表,有点类似于SQL中的笛卡尔乘积。

输出的结果:

欧阳晓晓 令狐冲
欧阳晓晓 任盈盈
诸葛菲菲 令狐冲
诸葛菲菲 任盈盈

2、where子句

     where子句的作用就是筛选元素,除了开始和结束位置,where子句几乎可以出现在LINQ表达式的任意位置。一个LINQ表达式中可以有where子句,也可以没有;可以有一个,可以有多个;多个where子句之间的关系相当于逻辑“与”,每个where子句可以包含1个或多个逻辑表达式,这些条件成为“谓词”,多个谓词之间用布尔运算符隔开,比如逻辑“与”用&&,逻辑“或”用||,而不是用SQL中的AND或OR。

2.1 常见的where子句查询

            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
                                                   };

            //查询名字是3个字或者姓“令”的,但年龄大于20的客户
            var query = from customer in clist
                        where (customer.Name.Length == 3 || customer.Name.Substring(0, 1) == "")
                        && customer.Age > 20
                        select customer;

            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }

结果:

姓名:令狐冲 年龄:23 电话:1380524****

2.2 在where子句中使用自定义函数

        private void whereExpDemo()
        {
            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
                                                   };

            //查询名字是3个字并且姓“令”的客户
            var query = from customer in clist
                        where (customer.Name.Length == 3 && CheckName(customer.Name))
                        select customer;

            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }
        }
        private bool CheckName(string name)
        {
            if (name.Substring(0, 1) == "")
                return true;
            else
                return false;
        }

结果:

姓名:令狐冲 年龄:23 电话:1380524****

 2.3 动态谓词的筛选

    上面的几个例子都是给定了查询谓词然后进行查询,有时候谓词的数量可能并不固定,是随情况变化的。例如:一组名字可能是运行时动态指定的。

            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
                                                   };

            //定义动态的谓词数组,这个数组应该由实际运行环境生成
            string[] names = { "令狐冲", "任盈盈", "杨过", "小龙女", "欧阳晓晓" };

            //查询在给定谓词数组里存在的客户
            var query = from customer in clist
                        where names.Contains(customer.Name)
                        select customer;

            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }

 结果:

姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:令狐冲 年龄:23 电话:1380524****

3. select子句

    LINQ表达式的结果是使用select子句获得的。select子句可以对数据进行转换,这个过程称为“投影”。select子句产生的类容,取决于前面的所有子句及其自身表达式执行后的结果。

3.1 输出查询结果

最简单的select就是直接输出from子句建立的那个范围变量:

            var query = from customer in clist
                        where names.Contains(customer.Name)
                        select customer;

也可以输出范围变量类型中得某个属性:

                        select customer.Name;

或者修改一下再输出:

select customer.Name.Replace("gg","mm");

或者干脆使用一个自定义的函数,把范围变量传进去,输出处理后的结果:

select MyFunction(customer.Name);

3.2 对查询结果进行投影

        public class MyCustomerInfo
        {
            public string Name { get; set; }
            public string Tel { get; set; }
        }
        private void whereExpDemo()
        {
            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"}
                                                   };

            //定义动态的谓词数组,这个数组应该由实际运行环境生成
            string[] names = { "令狐冲", "任盈盈", "杨过", "小龙女", "欧阳晓晓" };

            //查询在给定谓词数组里存在的客户
            var query = from customer in clist
                        where customer.Age < 30
                        select new MyCustomerInfo { Name = customer.Name, Tel = customer.Tel };

            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 电话:{1} 类型{2}", ci.Name, ci.Tel,ci.GetType().FullName);
            }
        }

上例中,在select子句中用对象初始化器生成了新的数据类型,从而进行了数据转换,使元素变成了MyCustomerInfo类型。

姓名:上官飘飘 电话:1592842**** 类型LinqDemo.Form1+MyCustomerInfo
姓名:令狐冲 电话:1380524**** 类型LinqDemo.Form1+MyCustomerInfo

4. group子句

    按照语法的规定,LINQ表达式必须以from子句开头,以select或group子句结束,所以除了使用select子句外,也可以使用guoup子句来返回元素分组后的结果。group子句返回的是一个IGrouping<TKey,TElement>泛型接口的对象集合,下面先了解下这个接口。

4.1 IGrouping<TKey,TElement>泛型接口

    这个接口表示具有公共键的对象集合,它的原型如下:

public interface IGrouping<TKey, TElement> : IEnumerable<TElement>, 
    IEnumerable

   TKey是键的对象类型,在用于group子句的时候,数据类型会有编译器推断出来,它一般用于存储分组的键值;TElement是指的对象类型,用于存储分组的结果,变量基于这个接口的类型就是遍历这个值。

4.2 分组查询

    分组查询对于关系型数据库是非常常见的一种操作,但在没有LINQ之前,对内存的对象进行分组却是一件非常麻烦的事情。现在,在LINQ表达式中只需要使用group子句就可以轻松完成对内存对象的分组。

            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"}
                                                   };

            //按照名字的前2个字进行分组
            var query = from customer in clist
                        group customer by customer.Name.Substring(0, 2);

            foreach (IGrouping<string,CustomerInfo> group in query)
            {
                Console.WriteLine("分组键:{0}",group.Key);
                foreach (var ci in group)
                {
                    Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel);
                }
                Console.WriteLine("***************************************");
            }

上例代码,按照form子句建立的范围变量customer的Name属性的前两个字作为键值进行分组。所以TKey的类型是一个字符串类型。

分组键:欧阳
姓名:欧阳晓晓 电话:1330708****
姓名:欧阳锦鹏 电话:1330708****
***************************************
分组键:上官
姓名:上官飘飘 电话:1592842****
姓名:上官无忌 电话:1380524****
***************************************

再看一个分组的例子:

            //按照年龄是否大于20分组
            var query = from customer in clist
                        group customer by customer.Age > 20;

            foreach (var group in query)
            {
                Console.WriteLine("分组键:{0}",group.Key);
                foreach (var ci in group)
                {
                    Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel);
                }
                Console.WriteLine("***************************************");
            }

group子句用了一个布尔表达式,所以IGrouping<TKey,TElement>的TKey变成了一个bool型。并且循环遍历的时候可以用var代替IGrouping的声明:

foreach (var group in query)
分组键:True
姓名:欧阳晓晓 电话:1330708****
姓名:欧阳锦鹏 电话:1330708****
姓名:上官无忌 电话:1380524****
***************************************
分组键:False
姓名:上官飘飘 电话:1592842****
***************************************

5. into子句

    into子句作为一个临时标识符,用于select,group,join子句中。

  List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"}
                                                   };

            //按照名字的前两个字进行分组,再用分组Key进行排序
            var query = from customer in clist
                        group customer by customer.Name.Substring(0, 2) into gpcustomer
                        orderby gpcustomer.Key descending
                        select gpcustomer;
            Console.WriteLine("into 用于group子句");
            foreach (var group in query)
            {
                Console.WriteLine("分组键:{0}", group.Key);
                foreach (var ci in group)
                {
                    Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel);
                }
                Console.WriteLine("***************************************");
            }

            var query2 = from customer in clist
                         select new { NewName = customer.Name, NewAge = customer.Age } into newCustomer
                         orderby newCustomer.NewAge
                         select newCustomer;

            Console.WriteLine("into 用于select子句");
            foreach (var ci in query2)
            {
                Console.WriteLine("{0} 年龄:{1}", ci.NewName, ci.NewAge);
            }

       into子句提供了一个临时标识符,它存储了into子句前面的查询内容,使它后面的子句可以方便的使用,对其进行再次查询,投影等操作。
执行结果:

into 用于group子句
分组键:上官
姓名:上官飘飘 电话:1592842****
姓名:上官无忌 电话:1380524****
***************************************
分组键:欧阳
姓名:欧阳晓晓 电话:1330708****
姓名:欧阳锦鹏 电话:1330708****
***************************************
into 用于select子句
上官飘飘 年龄:17
上官无忌 年龄:23
欧阳晓晓 年龄:35
欧阳锦鹏 年龄:35

6. 排序子句

      LINQ可以按元素的一个或多个属性对元素进行排序。LINQ表达式的排序方式分为OrderBy、OrderByDescending、ThenBy、ThenByDescending这四种。

6.1 OrderBy和OrderByDescending

    OrderBy用于按元素的值进行升序,语法:

orderby 用于排序的元素的表达式

   OrderByDescending用于按元素的值进行降序,语法:

orderby 用于排序的元素的表达式 descending
            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"}
                                                   };

            //按照年龄升序
            var query = from customer in clist
                        orderby customer.Age
                        select customer;
            Console.WriteLine("按年龄升序排列");
            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }
            //按照年龄降序
            var query2 = from customer in clist
                        orderby customer.Age descending
                        select customer;
            Console.WriteLine("\n按年龄降序排列");
            foreach (var ci in query2)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }

运行结果:

按年龄升序排列
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:上官无忌 年龄:23 电话:1380524****
姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:欧阳锦鹏 年龄:35 电话:1330708****

按年龄降序排列
姓名:欧阳晓晓 年龄:35 电话:1330708****
姓名:欧阳锦鹏 年龄:35 电话:1330708****
姓名:上官无忌 年龄:23 电话:1380524****
姓名:上官飘飘 年龄:17 电话:1592842****

6.2 ThenBy和ThenByDescending

    ThenBy和ThenByDescending用于对元素进行次要排序。基本语法:

orderby 用于排序的元素表达式,用于排序的元素表达式
orderby 用于排序的元素表达式,用于排序的元素表达式 descending
            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"}
                                                   };

            //按照年龄升序,再按名字的字数次要排序
            var query = from customer in clist
                        orderby customer.Age,customer.Name.Length
                        select customer;
            Console.WriteLine("按年龄排列,按名字字数进行次要排序");
            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }
            //按照年龄升序,再按名字的字数降序次要排序
            var query2 = from customer in clist
                        orderby customer.Age, customer.Name.Length descending
                        select customer;
            Console.WriteLine("\n按年龄排列,按名字字数进行降序次要排序");
            foreach (var ci in query2)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }
            //按照年龄升序,再按名字的字数降序要排序,在按电话号码进行第三条件排序
            var query3 = from customer in clist
                         orderby customer.Age, customer.Name.Length,customer.Tel
                         select customer;
            Console.WriteLine("\n按年龄,名字字数,电话号码排序");
            foreach (var ci in query3)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }

执行结果:

按年龄排列,按名字字数进行次要排序
姓名:郭靖 年龄:17 电话:1330708****
姓名:黄蓉 年龄:17 电话:1300524****
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:欧阳晓晓 年龄:35 电话:1330708****

按年龄排列,按名字字数进行降序次要排序
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:郭靖 年龄:17 电话:1330708****
姓名:黄蓉 年龄:17 电话:1300524****
姓名:欧阳晓晓 年龄:35 电话:1330708****

按年龄,名字字数,电话号码排序
姓名:黄蓉 年龄:17 电话:1300524****
姓名:郭靖 年龄:17 电话:1330708****
姓名:上官飘飘 年龄:17 电话:1592842****
姓名:欧阳晓晓 年龄:35 电话:1330708****

 7. let子句

     let子句用于在LINQ表达式中存储子表达式的计算结果。let子句创建一个范围变量来存储结果,变量被创建后,不能修改或把其他表达式的结果重新赋值给它。此范围变量可以再后续的LINQ子句中使用。

            List<CustomerInfo> clist = new List<CustomerInfo> { 
                                                   new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
                                                   new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"},
                                                   new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"}
                                                   };

            //姓“郭”或“黄”的客户
            var query = from customer in clist
                        let g = customer.Name.Substring(0,1)
                        where g == "" || g == ""
                        select customer;
            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel);
            }         
          

使用let 建立了个范围变量,这个范围变量在后续的where子句中使用,如果不使用let子句,where子句的表达式将写成这样:

where customer.Name.Substring(0, 1) == "" || customer.Name.Substring(0, 1) == ""
姓名:郭靖 年龄:17 电话:1330708****
姓名:黄蓉 年龄:17 电话:1300524****

8. join子句

    如果一个数据源中元素的某个属性可以跟另一个数据源中元素的属性进行相等比较,那么这两个数据源可以用join子句进行关联。jion子句用equals关键字进行比较,而不是常见的==。

      List<CustomerInfo> clist = new List<CustomerInfo> 
            { 
               new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"},
               new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"},
               new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"},
               new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"}
            };

            List<CustomerTitle> titleList = new List<CustomerTitle> 
            { 
               new CustomerTitle{ Name="欧阳晓晓", Title="歌手"},
               new CustomerTitle{ Name="郭靖", Title="大侠"},
               new CustomerTitle{ Name="郭靖", Title="洪七公徒弟"},
               new CustomerTitle{ Name="黄蓉", Title="才女"},
               new CustomerTitle{ Name="黄蓉", Title="丐帮帮主"}
            };

            //根据姓名进行内部联接
            Console.WriteLine("内部联接");
            var query = from customer in clist
                        join title in titleList
                        on customer.Name equals title.Name
                        select new { Name = customer.Name, Age = customer.Age, Title = title.Title };
            foreach (var ci in query)
            {
                Console.WriteLine("姓名:{0} 年龄:{1} {2}", ci.Name, ci.Age, ci.Title);
            }
            //根据姓名进行分组联接
            Console.WriteLine("\n根据姓名进行分组联接");
            var query2 = from customer in clist
                         join title in titleList
                         on customer.Name equals title.Name into tgroup
                         select new { Name = customer.Name, Titles = tgroup };
            foreach (var g in query2)
            {
                Console.WriteLine(g.Name);
                foreach (var g2 in g.Titles)
                {
                    Console.WriteLine("   {0}", g2.Title);
                }
            }
            //根据姓名进行 左外部联接
            Console.WriteLine("\n左外部联接");
            var query3 = from customer in clist
                         join title in titleList
                         on customer.Name equals title.Name into tgroup
                         from subTitle in tgroup.DefaultIfEmpty()
                         select new { Name = customer.Name, Title = (subTitle == null ? "空缺" : subTitle.Title) };
            foreach (var ci in query3)
            {
                Console.WriteLine("姓名:{0} {1} ", ci.Name, ci.Title);
            }

要仔细理解上例的,内联接,分组联接,以及左联接。

内部联接
姓名:欧阳晓晓 年龄:35 歌手
姓名:郭靖 年龄:17 大侠
姓名:郭靖 年龄:17 洪七公徒弟
姓名:黄蓉 年龄:17 才女
姓名:黄蓉 年龄:17 丐帮帮主

根据姓名进行分组联接
欧阳晓晓
   歌手
上官飘飘
郭靖
   大侠
   洪七公徒弟
黄蓉
   才女
   丐帮帮主

左外部联接
姓名:欧阳晓晓 歌手 
姓名:上官飘飘 空缺 
姓名:郭靖 大侠 
姓名:郭靖 洪七公徒弟 
姓名:黄蓉 才女 
姓名:黄蓉 丐帮帮主

三、Linq to Objects之延期执行方法

    LINQ to Objects是 LINQ的基础,而 LINQ to SQL、 LINQ to XML是中间 LINQ提供程序,他们主要是把数据源转换成 LINQ to Objects兼容的类型,以便 LINQ to Objects进行操作。 LINQ to Objects就是直接对IEnumerable或泛型IEnumerable<T>集合进行查询。LINQ表达式是LINQ标准查询运算符的一部分,而LINQ标准查询运算符则是LINQ to Objects的基础。它们是一组静态方法,被定义在System.Linq.Enumerable和System.Linq.Queryable类中。这两个类中方法基本一致,唯一的不同点是System.Linq.Queryable类中方法会把LINQ表达式拆解成表达式目录树,其他一些Linq提供程序可以将这个表达式目录树翻译成查询语句,比如SQL语句,然后再执行相关操作。

    本文主要学习System.Linq.Enumerable的扩展方法,这些方法按照执行的行为不同,可以分为延期执行和立即执行。延期执行的运算符在枚举时被执行,下面要学习的就是延期执行方法的一部分。

1,Take 方法

    Take方法用于从一个序列的开头返回指定数量的元素

            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };

            //直接输出前3个元素
            Console.WriteLine("Take方法直接输出前3个元素");
            foreach (var name in names.Take(3))
            {
                Console.WriteLine(name);
            }
            var query = from n in names
                        where n.Length == 2
                        select n;
            Console.WriteLine("\nTake方法输出查询结果的前1个元素");
            foreach (var s in query.Take(1))
            {
                Console.WriteLine(s);
            }

输出结果:

Take方法直接输出前3个元素
郭靖
李莫愁
欧阳晓晓

Take方法输出查询结果的前1个元素
郭靖
 

2,TakeWhile 方法

    TakeWhile方法获取序列中从开头起符合条件的元素,直到遇到不符合条件的元素为止的所有元素。条件代理部分有两种形式:

Func<TSource, bool> predicate

Func<TSource, int, bool> predicate 第二个参数是元素的索引

注意:当条件为假时,就停止了,后面的元素不会输出。

            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };

            //输出名字小于4个字的元素
            var takeNames = names.TakeWhile(n => n.Length < 4);
            foreach (var name in takeNames)
            {
                Console.WriteLine(name);
            }
            Console.WriteLine("\nTakeWhile 带索引参数");
            //输出名字字数小于等于4 并且索引小于4的元素
            foreach (var name in names.TakeWhile((n, i) => n.Length <= 4 && i < 4))
            {
                Console.WriteLine(name);
            }

输出结果:

郭靖
李莫愁

TakeWhile 带索引参数
郭靖
李莫愁
欧阳晓晓
黄蓉

3,Skip 方法

   Skip方法用于跳过序列中指定个数的元素。

            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };

            //跳过前3个元素
            Console.WriteLine("Take方法跳过前3个元素");
            foreach (var name in names.Skip(3))
            {
                Console.WriteLine(name);
            }
            var query = from n in names
                        where n.Length == 2
                        select n;
            Console.WriteLine("\nTake方法跳过查询结果的前1个元素");
            foreach (var s in query.Skip(1))
            {
                Console.WriteLine(s);
            }

输出结果:

Take方法跳过前3个元素
黄蓉
黄药师

Take方法跳过查询结果的前1个元素
黄蓉

4,SkipWhile 方法

    SkipWhile 方法用于只要满足指定的条件,就跳过序列中得元素。

注意当遇到条件为假时,就停止跳越了输出剩余的所有元素

 
            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };

            Console.WriteLine("SkipWhile跳过名字为2个字的元素");
            foreach (var name in names.SkipWhile(n => n.Length == 2))
            {
                Console.WriteLine(name);
            }

            Console.WriteLine("\nSkipWhile跳过名字小于4个字,并且索引小于2");
            foreach (var s in names.SkipWhile((n, i) => n.Length < 4 && i < 2))
            {
                Console.WriteLine(s);
            }

输出结果:

SkipWhile跳过名字为2个字的元素
李莫愁
欧阳晓晓
黄蓉
黄药师

SkipWhile跳过名字小于4个字,并且索引小于2
欧阳晓晓
黄蓉
黄药师

5,Reverse 方法

    Reverse 方法用于反转序列中的元素。

            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };

            foreach (var name in names.Reverse())
            {
                Console.WriteLine(name);
            }

输出结果:

黄药师
黄蓉
欧阳晓晓
李莫愁
郭靖

6,Distinct 方法   

    Distinct 方法用于去除重复元素。

            string[] names = { "郭靖", "郭靖", "李莫愁", "欧阳晓晓", "欧阳晓晓", "黄蓉", "黄药师" };

            Console.WriteLine("含有重复元素的数组");
            foreach (var name in names)
            {
                Console.Write(name + " ");
            }
            Console.WriteLine("\n\n去除重复元素的数组");
            foreach (var name in names.Distinct())
            {
                Console.Write(name + " ");
            }
 

输出结果:

含有重复元素的数组
郭靖 郭靖 李莫愁 欧阳晓晓 欧阳晓晓 黄蓉 黄药师 

去除重复元素的数组
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师

自定义IEqualityComparer<T>接口的相等比较器

 
        public class MyEqualityComparer<T> : IEqualityComparer<T>
        {
            #region IEqualityComparer<T> 成员

            public bool Equals(T x, T y)
            {
                string temp = x as string;
                if (temp != null)
                {
                    if (temp == "欧阳晓晓") //对"欧阳晓晓"不过滤
                        return false;
                }
                if (x.GetHashCode() == y.GetHashCode())
                    return true;
                else
                    return false;
            }

            public int GetHashCode(T obj)
            {
                return obj.GetHashCode();
            }

            #endregion
        }
        private void DistinctDemo()
        {
            string[] names = { "郭靖", "郭靖", "李莫愁", "欧阳晓晓", "欧阳晓晓", "黄蓉", "黄药师" };

            Console.WriteLine("含有重复元素的数组");
            foreach (var name in names)
            {
                Console.Write(name + " ");
            }
            Console.WriteLine("\n\n去除重复元素的数组,实现自定义IEqualityComparer<T>");
            foreach (var name in names.Distinct(new MyEqualityComparer<string>()))
            {
                Console.Write(name + " ");
            }
        }
 

输出结果:

含有重复元素的数组
郭靖 郭靖 李莫愁 欧阳晓晓 欧阳晓晓 黄蓉 黄药师 

去除重复元素的数组,实现自定义IEqualityComparer<T>
郭靖 李莫愁 欧阳晓晓 欧阳晓晓 黄蓉 黄药师

7,Union 方法

    Union 方法 用于合并两个序列,并去掉重复元素。

 
            string[] names = { "郭靖", "郭靖", "李莫愁", "欧阳晓晓", "欧阳晓晓", "黄蓉", "黄药师" };

            Console.WriteLine("含有重复元素的数组");

            foreach (var name in names)
            {
                Console.Write(name + " ");
            }
            Console.WriteLine("\n\n去除重复元素的数组,实现自定义IEqualityComparer<T>");
            foreach (var name in names.Distinct(new MyEqualityComparer<string>()))
            {
                Console.Write(name + " ");
            }
 

输出结果:

合并后的元素
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师 杨过 

自定义IEqualityComparer<T>接口的相等比较器

            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
            string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };

            Console.WriteLine("合并后的元素");
            foreach (var name in names.Union(names2,new MyEqualityComparer<string>()))
            {
                Console.Write(name + " ");
            }
 

输出结果:

合并后的元素
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师 杨过 欧阳晓晓

8,Concat 方法

  Concat 方法 用于连接两个序列,与Union不同,它不会过滤重复的元素。

 
            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
            string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };

            Console.WriteLine("合并后的元素");
            foreach (var name in names.Concat(names2))
            {
                Console.Write(name + " ");
            }
 

输出元素:

合并后的元素
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师 郭靖 杨过 欧阳晓晓 

9,Intersect 方法

  Intersect 方法用于生成两个序列的交集。

 
            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
            string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };

            Console.WriteLine("相交的元素");
            foreach (var name in names.Intersect(names2))
            {
                Console.Write(name + " ");
            }
 

输出结果:

相交的元素
郭靖 欧阳晓晓

自定义IEqualityComparer<T>

 
        public class MyEqualityComparer<T> : IEqualityComparer<T>
        {
            #region IEqualityComparer<T> 成员

            public bool Equals(T x, T y)
            {
                string temp = x as string;
                if (temp != null)
                {
                    if (temp == "欧阳晓晓") //对"欧阳晓晓"不过滤
                        return false;
                }
                if (x.GetHashCode() == y.GetHashCode())
                    return true;
                else
                    return false;
            }

            public int GetHashCode(T obj)
            {
                return obj.GetHashCode();
            }

            #endregion
        }
 
           string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
            string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };

            Console.WriteLine("相交的元素");
            foreach (var name in names.Intersect(names2,new MyEqualityComparer<string>()))
            {
                Console.Write(name + " ");
            }

输出结果:

相交的元素
郭靖

10,Except 方法

 Except 方法用于生成两个序列的差集。

注意:返回是第一个数组里,去掉指定数组里的元素后,剩下的一个序列。

它和Intersect方法不是互补的,不要搞混了。下面的“杨过”就不会输出。因为它是指定数组里的元素,和源数组一毛钱关系都没有。

 
            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
            string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };

            Console.WriteLine("2个数组的不同元素");
            foreach (var name in names.Except(names2))
            {
                Console.Write(name + " ");
            }

输出结果:

2个数组的不同元素
李莫愁 黄蓉 黄药师

运用自定义IEqualityComparer<T>指定比较器。

 
            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
            string[] names2 = { "郭靖", "杨过", "欧阳晓晓" };

            Console.WriteLine("2个数组的不同元素");
            foreach (var name in names2.Except(names,new MyEqualityComparer<string>()))
            {
                Console.Write(name + " ");
            }

输出结果:

2个数组的不同元素
杨过 欧阳晓晓

11,Range 方法

    Range 方法用于生成指定范围的整数序列。在BS程序中,经常需要分页显示,在页面中需要显示页面号码的链接,用这个方法可以生成页码的数组。

由于没有this关键字,它是一个普通的静态方法。

            int istart = 1;//起始页码
            int iend = 12; //结束页码

            var pages = Enumerable.Range(1, iend - istart + 1);
            Console.WriteLine("输出页码");
            foreach (var n in pages)
            {
                Console.Write("[{0}] ", n);
            }

输出结果:

输出页码
[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12]

12,Repeat 方法

    Repeat 方法用于生成指定数量重复元素的序列。由于没有this关键字,它是一个普通的静态方法。

            var people = new { Name = "郭靖", Age = 35 };//定义一个匿名类型

            var peoples = Enumerable.Repeat(people, 4);

            Console.WriteLine("包含4个匿名元素:");
            foreach (var n in peoples)
            {
                Console.WriteLine("{0} {1} ", n.Name, n.Age);
            }

输出结果:

包含4个匿名元素:
郭靖 35 
郭靖 35 
郭靖 35 
郭靖 35 

13,Empty 方法

    Empty 方法用于获取一个指定类型参数的空序列。由于没有this关键字,它是一个普通的静态方法。

           var s = Enumerable.Empty<string>();

            Console.WriteLine("序列的元素数:{0} ", s.Count());

输出结果:

序列的元素数:0 

14,DefaultIfEmpty 方法

    DefaultIfEmpty 方法用于获取序列,如果序列为空则添加一个类型的默认值。例如:如果元素为引用类型,添加null元素;元素为int类型,则添加int的默认值0。

 
            string[] names = { "郭靖", "李莫愁", "欧阳晓晓", "黄蓉", "黄药师" };
            var intempty = Enumerable.Empty<int>();//空的Int类型序列
            //没有找到元素的序列
            var empty = from n in names
                        where n.Length == 5
                        select n;
            Console.WriteLine("DefaultIfEmpty 返回有内容的序列");
            foreach (var n in names)
            {
                Console.Write("{0} ", n);
            }
            Console.WriteLine("\nempty空序列元素数:{0}", empty.Count());
            Console.WriteLine("empty空序列应用DefaultIfEmpty 后的元素数:{0}", empty.DefaultIfEmpty().Count());
            Console.Write("empty空序列应用DefaultIfEmpty 后的元素值:");
            foreach (var n in empty.DefaultIfEmpty())
            {
                if (n == null)
                    Console.Write("null");
            }
            Console.WriteLine("\n****************************************");
            Console.WriteLine("intempty空序列元素数:{0}", intempty.Count());
            Console.WriteLine("intempty空序列应用DefaultIfEmpty 后的元素数:{0}", intempty.DefaultIfEmpty().Count());
            Console.Write("intempty空序列应用DefaultIfEmpty 后的元素值:");
            foreach (var n in intempty.DefaultIfEmpty())
            {
                Console.Write(n);
            }

输出结果:

DefaultIfEmpty 返回有内容的序列
郭靖 李莫愁 欧阳晓晓 黄蓉 黄药师 
empty空序列元素数:0
empty空序列应用DefaultIfEmpty 后的元素数:1
empty空序列应用DefaultIfEmpty 后的元素值:null
****************************************
intempty空序列元素数:0
intempty空序列应用DefaultIfEmpty 后的元素数:1
intempty空序列应用DefaultIfEmpty 后的元素值:0

这个方法还可以指定一个自定义的默认值。

            var intempty = Enumerable.Empty<int>();//空的Int类型序列
            Console.Write("int 类型自定义默认值:");
            foreach (var i in intempty.DefaultIfEmpty(200))
            {
                Console.Write(i);
            }

输出结果:

int 类型自定义默认值:200

7、Cast 方法

    Cast 方法用于按照TResult类型转换IEnumerable序列的集合。

            //ArrayList没有实现IEnumerable<T>接口
            ArrayList names = new ArrayList();
            names.Add("郭靖");
            names.Add("李莫愁");
            names.Add("欧阳晓晓");

            IEnumerable<string> newNames = names.Cast<string>();
            foreach (var s in newNames)
            {
                Console.WriteLine(s);
            }

输出结果:

郭靖
李莫愁
欧阳晓晓 

8,OfType 方法

    OfType 方法用于根据TResult类型筛选IEnumerable类型序列的元素。它的用途和Cast方法类似,但OfType方法如果遇到不能强制转换成TResutl的类型,会丢弃该元素,而不会出现运行错误。

            //ArrayList没有实现IEnumerable<T>接口
            ArrayList names = new ArrayList();
            names.Add("郭靖");
            names.Add("李莫愁");
            names.Add(100);
            names.Add(new Stack());
            names.Add("欧阳晓晓");

            IEnumerable<string> newNames = names.OfType<string>();
            foreach (var s in newNames)
            {
                Console.WriteLine(s);
            }

输出结果:

郭靖
李莫愁
欧阳晓晓

9,AsEnumerable方法

    AsEnumerable方法根据元素的类型转换为泛型IEnumerable<T>类型。

MSDN上的一个例子,AsEnumerable用于隐藏自己定义的和IEnumerable里的扩展方法同名的方法。

 
        public class MyList<T> : List<T>
        {
            public IEnumerable<T> Where(Func<T, bool> predicate)
            {
                Console.WriteLine("In MyList of Where");
                return Enumerable.Where(this, predicate);
            }
        }
        private void AsEnumerableDemo()
        {
            MyList<string> list = new MyList<string>() { "郭靖", "黄蓉", "黄药师" };

            var query1 = list.Where(n => n.Contains(""));
            Console.WriteLine("query1 created");
            
            var query2 = list.AsEnumerable().Where(n => n.Contains(""));
            Console.WriteLine("query2 created");
        }

运行结果:

In MyList of Where
query1 created
query2 created

AsEnumerable方法经常用于Linq To SQL查询,将IQueryable<T>转换成IEnumerable<T>接口。因为一些扩展方法,如Reverse等,虽然在IQueryable<T>里有定义,但并不能将其翻译成对应的SQL语句,所以运行时会报错。用AsEnumerable方法转换成IEnumerable<T>后,实际的数据就在内存中操作。关于IQueryable<T>调用AsEnumerable背后的转换本质,有待进一步考证。

四、 IEnumberable & IQueryable 区别

在应用到IEnumberable 和IQueryable两个接口时,代码往往很相似,从而造成了很多困惑,然后事实上他们两是有很大的区别的,各种都有自己特定的使用场景。下面是IEnumberable和IQueryable的属性对比:

  IEnumerable  IQueryable
Namespace System.Collections Namespace System.Linq Namespace
继承于 No base interface 继承于 IEnumerable
Deferred Execution 支持 支持
Lazy Loading 不支持 支持
如何工作 当从数据库中查询数据,IEnumberable在服务器端执行查询操作,下载数据到客户端的内存中,然后再筛选数据,因此这个操作需要更多的工作而变得缓慢。 当从数据库中查询数据,IQueryable在服务器端根据所有的filter条件执行查询操作,因此该操作需要更少的工作而运行快。
适用于 LINQ to Object and LINQ to XML queries. LINQ to SQL queries.
自定义查询 不支持 支持使用CreateQuery 和Execute 方法。
Extension mehtod
parameter
Extension methods supported in IEnumerable takes functional objects. Extension methods supported in IEnumerable takes expression objects i.e. expression tree.
使用场合 当从内存中的数据集合(如LIst,Array etc)查询数据的时候 当查询非内存中的数据集合(如远程数据库,service等)时。
最常使用 内存遍历 Paging

 

参考资料:http://www.cnblogs.com/xiashengwang/archive/2012/07/29/2613940.html

推荐阅读