在上一篇中简单介绍了 Linq 的入门级用法,这一篇尝试讲解一些更加深入的使用方法,与前一篇的结构不一样的地方是,这一篇我会先介绍Linq里的支持方法,然后以实际需求为引导,分别以方法链的形式和类 SQL 的形式写出来。
前言在上一篇中简单介绍了 Linq 的入门级用法,这一篇尝试讲解一些更加深入的使用方法,与前一篇的结构不一样的地方是,这一篇我会先介绍 Linq 里的支持方法,然后以实际需求为引导,分别以方法链的形式和类 SQL 的形式写出来。
前置概念介绍Predicate
谓词、断言,等价于Func
即返回 bool 的表达式Expression
表达式树,这个类很关键,但是在这里会细说,我们会讲它的一个特殊的泛型类型:Expression
这个在某些数据源的查询中十分重要,它代表 lambda 表达式中一种特殊的表达式,即没有大括号和return
关键字的那种。
我们先准备两个类:
- Student/学生类:
/// /// 学生 /// public class Student { /// /// 学号 /// public int StudentId { get; set; } /// /// 姓名 /// public string Name { get; set; } /// /// 班级 /// public string Class { get; set; } /// /// 年龄 /// public int Age { get; set; } }
- Subject/科目类:
/// /// 科目 /// public class Subject { /// /// 名称 /// public string Name { get; set; } /// /// 年级 /// public string Grade { get; set; } /// /// 学号 /// public int StudentId { get; set; } /// /// 成绩 /// public int Score { get; set; } }
Subject 和 Student 通过学号字段一一关联,实际工作中数据表有可能会设计成这。
那么先虚拟两个数据源:IEnumerable students
和 IEnumerable subjects
。先忽略这两个数据源的实际来源,因为在开发过程中数据来源有很多种情况,有数据库查询出来的结果、远程接口返回的结果、文件读取的结果等等。不过最后都会整理成IEnumerable
的子接口或实现类的对象。
where 的方法声明:
public IEnumerable Where (this IEnumerable source, Func predicate)
可以看出不会转换数据类型,通过给定的 lambda 表达式或者一个方法进行过滤,获取返回 true 的元素。
示例:
// 获取年纪大于 10 但不大于 12 的同学们List results = students.Where(t=>t.Age >10 && t.Age p.Class).ToList();
OrderBy/OrderByDescending 进行排序,按条件升序/降序
它们是一对方法,一个是升序一个降序,其声明是一样的:
常用的是:
public static System.Linq.IOrderedEnumerable OrderBy (this IEnumerable source, Func keySelector);
示例:
//按年龄的升序排列:List results = students.OrderBy(p => p.Age).ToList();//按年龄的降序排列:List results = students.OrderByDescending(p => p.Age).ToList();
First/Last 获取数据源的第一个/最后一个
这组方法有两个常用的重载声明:
First:
// 直接获取第一个public static TSource First (this IEnumerable source);// 获取满足条件的第一个public static TSource First (this IEnumerable source, Func predicate);
Last:
// 直接获取最后一个public static TSource Last (this IEnumerable source);// 获取最后一个满足条件的元素public static TSource Last (this IEnumerable source, Func predicate);
示例:
Student student = students.First();// 等价于 students[0];Student student = students.First(p=>p.Class == "一班");//获取数据源中第一个一班的同学Student student = students.Last();//最后一个学生Student student = students.Last(p=>p.Class == "三班");//获取数据源中最后一个三班的同学
注意:
- 在某些数据源中使用 Last 会报错,因为对于一些管道类型的数据源或者说异步数据源,程序无法确认最后一个元素的位置,所以会报错。解决方案:先使用 OrderBy 对数据源进行一次排序,使结果与原有顺序相反,然后使用 First 获取
- 当数据源为空,或者不存在满足条件的元素时,调用这组方法会报错。解决方案:调用 FirstOrDefault/LastOrDefault,这两组方法在无法查询到结果时会返回一个默认值。
Any:是否存在元素满足条件
有两个版本,不过意思可能不太一样:
public static bool Any (this IEnumerable source);//数据源中是否有数据//================//是否存在满足条件的数据public static bool Any (this IEnumerable source, Func predicate);
All :是否都满足条件:
public static bool Any (this IEnumerable source, Func predicate);
示例:
// 是否有学生bool isAny = students.Any();// 是否有五班的同学bool isFive = students.Any(p=>p.Class == "五班");// 是否所有学生的年纪都不小于 9 岁bool isAll = students.All(p=>p.Age >= 9);
Skip 略过几个元素
Skip 一共有三个衍生方法:
第一个:Skip 自己: 略过几个元素,返回剩下的元素内容
public static IEnumerable Skip (this IEnumerable source, int count);
第二个:SkipLast,从尾巴开始略过几个元素,返回剩下的元素内容
public static IEnumerable SkipLast (this IEnumerable source, int count);
第三个:SkipWhile,跳过满足条件的元素,返回剩下的元素
public static IEnumerable SkipWhile (this IEnumerable source, Func predicate);
示例:
// 不保留前 10 个学生List results = students.Skip(10).ToList();// 不保留后 10 个学生List results = students.SkipLast(10).ToList();// 只要非一班的学生List results = students.SkipWhere(p=>p.Class=="一班").ToList();//上一行代码 等价于 = students.Where(p=>p.Class != "一班").ToList();
Take 选取几个元素
Take 与 Skip 一样也有三个衍生方法,声明的参数类型也一样,这里就不对声明做介绍了,直接上示例。
//选取前 10 名同学List results = students.Take(10).ToList();// 选取最后 10 名同学List results = students.TakeLast(10).ToList();//选取 一班的学生List results = students.TakeWhile(p=>p.Class=="一班").ToList();// 上一行 等价于 = students.Where(p=>p.Class=="一班").ToList();
在使用 Linq 写分页的时候,就是联合使用 Take 和 Skip 这两个方法:
int pageSize = 10;//每页 10 条数据int pageIndex = 1;//当前第一页List results = students.Skip((pageIndex-1)*pageSize).Take(pageSize).ToList();
其中 pageIndex 可以是任意大于 0 的数字。Take 和 Skip 比较有意思的地方就是,如果传入的数字比数据源的数据量大,根本不会爆粗,只会返回一个空数据源列表。
Select 选取官方对于 Select 的解释是,将序列中的每个元素投影到新的表单里。我的理解就是,自己 定义一个数据源单个对象的转换器,然后按照自己的方式对数据进行处理,选择出一部分字段,转换一部分字段。
所以按我的理解,我没找到 java8 的同效果方法。(实际上 java 用的是 map,所以没找到,:-D)
public static System.Collections.Generic.IEnumerable Select (this IEnumerable source, Func selector);
示例:
// 选出班级和姓名List results = students.Select(p => new{ p.Class, p.Name}).ToList();
简单运算操作
Linq 里有几个需要注意的简单运算操作,这部分在使用中很常见。
Max 选取最大的一个Max 获取数据源中最大的一个,不过只能是数字类型的,其他类型因为不能直接比较大小所以可以有替代方法,就是先排序取第一个。
以下是 Max 方法的两个重载版本:
public static int Max (this IEnumerable source);public static int Max (this IEnumerable source,Func selector);
示例:
//查询学生中最大的年纪是多少int maxAge = students.Select(t=>t.Age).Max();
Min 选取最小的一个
方法类似与 Max,不过与之不同的是获取最小的一个,不能应用于非数字类型。
示例:
// 查询学生中最小的年纪是多少int minAge = students.Select(t=> t.Age).Min();//=======int minAge = students.Min(p=>p.Age);
Average 求平均数
与 Max/Min 是一样类型的方法,依旧不能应用于非数字类型。
示例:
// 查询学生的评价年纪int averageAge = students.Select(t=>t.Age).Average();int averageAge = students.Average(p=>p.Age);
Sum 求和
对数据源进行求和或者对数据源的某个字段进行求和,还是不能对非数字类型进行求和
示例:
// 一个没有实际意义的求和,学生的年龄总和int sumAge = students.Select(t=>t.Age).Sum();//int sumAge = students.Sum(p=>p.Age);
Contains 是否包含某个元素
判断数据源中是否包含某个元素,返回一个 bool 值,如果包含则返回 true,如果不包含则返回 false。该方法有两个重载版本,一个是使用默认的Equals
方法,一个是指定一个相等性比较器实现类。
public static bool Contains (this IEnumerable source, TSource value);//传入相等性比较器的public static bool Contains (this IEnumerable source, TSource value, IEqualityComparer comparer);
值得注意的是,这里的相等比较器是一个接口,也就是说需要使用类来实现这个方法。通常在实际开发过程中,我们会在 TSource 这个数据源所代表的类上增加 IEqualityCompare 的实现。
示例 1:
Student student1 = new Student();// 初始化一个学生类Student student2 = students.First();// 从数据源中取一个bool isContains = students.Contains(student1);// 返回 false,bool isContains2 = students.Contains(student2);// 返回 true
说明: 类的默认相等比较是比较是否是同一个对象,即返回的
示例 2:
创建一个相等性比较器,值得注意的是,相等性比较器有两个方法,一个是比较元素是否相等,一个是返回元素的 HashCode,这两个方法必须在判断元素是否相等上保持结果一致。
public class StudentEqualityCompare: IEqualityComparer{ public bool Equals(Student x, Student y) { // 省略逻辑 } public int GetHashCode(Student obj) { //省略逻辑 }}
使用:
StudentEqualityCompare compare = new StudentEqualityCompare();Student student = students.First();bool isContains = students.Contains(student, compare);
Count/LongCount 数量查询
这是一组行为一样的方法,就是对数据源进行计数,不同的是 Count 返回 int,LongCount 返回 long。
它们的声明有以下两种,这里选了 Count 的声明:
public static int Count (this IEnumerable source);public static int Count (this IEnumerable source, Func predicate);
示例:
int count = students.Count();//返回一共有多少个学生int count = students.Count(p=>p.Class=="一班");// 统计一班一共有多少学生
同类型数据源的操作
之前介绍了单个数据源的操作方法,这些方法不会让数据源发生变化,更多的对数据源进行过滤和选择或者统计。现在介绍几个对多个数据源进行操作的方法。
Union 联合另一个同类型的数据源联合另一个数据源,意思就是把两个数据源合并到一个里面,去掉重复的元素,只保留不重复的元素,并返回这个结果集。
与 Contains 方法差不多,这个方法有两个重载的版本:
public static IEnumerable Union (this IEnumerable first, IEnumerable second);public static IEnumerable Union (this IEnumerable first, IEnumerable second, IEqualityComparer comparer);
示例:
先假设一个业务场景:
学校举办运动会,现在教务处收到了田径组 500 米跑的报名名单和跳远的报名名单,需要看看一共有哪些学生报名了这两项赛事。
// 省略数据源,田径组的名单IEnumerable students1 = new List();//省略数据源来源,跳远组的名单IEnumerable students2 = new List();List all = students1.Union(student2).ToList();
这时候简单统计了一下所有人,但是后来教务处在核对的时候,发现有的人名重复了,需要判断是否是一个人,这时候就必须创建一个相等比较器了。
List all = students1.Union(student2,compare).ToList();// 省略 compare 的实现,具体可参照 Contains 的比较器
Intersect 获取两个集合中都存在的数据
获取同时存在于两个集合中的元素,与 Union 类似。
方法的声明如下:
public static IEnumerable Intersect (this IEnumerable first, IEnumerable second);public static IEnumerable Intersect (this IEnumerable first, IEnumerable second, IEqualityComparer comparer);
示例:
继续之前的业务场景,现在教务处需要知道有哪些同学同时报名了两个比赛
List students = students1.Intersect(students2).ToList();
Except 获取只在第一个数据源中存在的数据
获取只存在于第一个集合的元素,从第一个集合中去除同时存在与第二个集合的元素,并返回。
方法的声明如下:
public static IEnumerable Except (this IEnumerable first, IEnumerable second);public static IEnumerable Except (this IEnumerable first, IEnumerable second, IEqualityComparer comparer);
示例:
继续业务描述,教务处要一份只报名了 500 米的学生名单:
List students = students1.Except(students2).ToList();
Reverse 翻转顺序
数据源中的元素原本有一定的顺序,这个方法可以将数据源中的顺序翻转过来,原本是最后一个的变成了第一个
,第一个变成了最后一个。
简单示例:
char[] apple = { 'a', 'p', 'p', 'l', 'e' };char[] reversed = apple.Reverse().ToArray();
Distinct 去重
对数据源进行去重,然后返回去重之后的结果。同样,这个方法有两个重载版本,一个有比较器,一个没有比较器。
// 不用比较器的public static IEnumerable Distinct (this IEnumerable source);// 设置比较器public static IEnumerable Distinct (this IEnumerable source, IEqualityComparer comparer);
示例:
先描述一个可能会出现的场景,每个班级在各个赛事组提交报名信息的时候有点混乱,500 米的负责老师把一个班的名单多录了一次,但是学生已经乱序了,现在需要把多录的去掉,也就是对数据进行去重。
List students = students1.Distinct();
多个类型数据源的操作
之前的方法基本都是对一个类型的数据源进行操作,不会涉及其他类型的数据源。现在介绍一下怎么关联多个类型的数据源,类似于 SQL 里的多表链接查询。
Join 关联两个数据源按照一定的逻辑将两个数据源关联到一起,然后选择出需要的数据。
方法有这几个重载版本:
public static IEnumerable Join (this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector);//public static IEnumerable Join (this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer comparer);
这个方法的参数比较多,我们大概介绍一下这个方法的所有参数:
类型参数
TOuter 第一个序列中的元素的类型。
TInner 第二个序列中的元素的类型。
TKey 选择器函数返回的键的类型。
TResult 结果元素的类型。
参数
outer IEnumerable 要联接的第一个序列。
inner IEnumerable 要与第一个序列联接的序列。
outerKeySelector Func 用于从第一个序列的每个元素提取联接键的函数。
innerKeySelector Func 用于从第二个序列的每个元素提取联接键的函数。
resultSelector Func 用于从两个匹配元素创建结果元素的函数。
comparerIEqualityComparer 用于对键进行哈希处理和比较的 IEqualityComparer。
示例:
假设前天语文老师组织了一场考试,因为是模拟正式考试,所以答题纸上学生都只写了学号,现在需要把考试成绩和学生们联系在一起
List results = students.Join(subjects, p => p.StudentId, s => s.StudentId, (p, s) => new { Student = p, Subject = s }).ToList();/**返回一个学生和科目的匿名对象,不过被我用 object 接了,这里会有一个问题,如果有兴致可以提前了解一下 C#的 var 关键字和匿名对象,这部分将会放在 C#基础系列补全篇讲解*/
GroupJoin 关联两个数据源,并分组
基于键值等同性将两个序列的元素进行关联,并对结果进行分组。以上是官方介绍,我在开发过程中并没有使用过这个方法,不过这个方法完全可以认为是 Join 和 Group 的组合体,即先进行了一次 Join 然后又对数据进行一次分组。
方法声明:
// 使用默认比较器public static IEnumerable GroupJoin (this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector);//设置比较器public static IEnumerable GroupJoin (this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer comparer);
类型参数
TOuter 第一个序列中的元素的类型。
TInner 第二个序列中的元素的类型。
TKey 键选择器函数返回的键的类型。
TResult 结果元素的类型。
参数
outer IEnumerable 要联接的第一个序列。
inner IEnumerable 要与第一个序列联接的序列。
outerKeySelector Func 用于从第一个序列的每个元素提取联接键的函数。
innerKeySelector Func 用于从第二个序列的每个元素提取联接键的函数。
resultSelector Func 用于从第一个序列的元素和第二个序列的匹配元素集合中创建结果元素的函数。
comparer IEqualityComparer 用于对键进行哈希处理和比较的 IEqualityComparer。
以下是官方给的示例:
class Person{ public string Name { get; set; }}class Pet{ public string Name { get; set; } public Person Owner { get; set; }}public static void GroupJoinEx1(){ Person magnus = new Person { Name = "Hedlund, Magnus" }; Person terry = new Person { Name = "Adams, Terry" }; Person charlotte = new Person { Name = "Weiss, Charlotte" }; Pet barley = new Pet { Name = "Barley", Owner = terry }; Pet boots = new Pet { Name = "Boots", Owner = terry }; Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte }; Pet daisy = new Pet { Name = "Daisy", Owner = magnus }; List people = new List { magnus, terry, charlotte }; List pets = new List { barley, boots, whiskers, daisy }; // Create a list where each element is an anonymous // type that contains a person's name and // a collection of names of the pets they own. var query = people.GroupJoin(pets, person => person, pet => pet.Owner, (person, petCollection) => new { OwnerName = person.Name, Pets = petCollection.Select(pet => pet.Name) }); foreach (var obj in query) { // Output the owner's name. Console.WriteLine("{0}:", obj.OwnerName); // Output each of the owner's pet's names. foreach (string pet in obj.Pets) { Console.WriteLine(" {0}", pet); } }}/* This code produces the following output: Hedlund, Magnus: Daisy Adams, Terry: Barley Boots Weiss, Charlotte: Whiskers*/
以上是关于 Linq 的所有方法内容,但是这仍然不是 Linq 的全部。后续还会有一篇关于 Linq 的另一种查询方式的内容文章。
阅读全文: http://gitbook.cn/gitchat/activity/5e8eba2d6ab5775c2588e9f2
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。