C# 基础知识系列-7 Linq详解

对着背影说爱祢 提交于 2020-04-06 10:30:13

前言

在上一篇中简单介绍了Linq的入门级用法,这一篇尝试讲解一些更加深入的使用方法,与前一篇的结构不一样的地方是,这一篇我会先介绍Linq里的支持方法,然后以实际需求为引导,分别以方法链的形式和类SQL的形式写出来。

前置概念介绍

  1. Predicate<T> 谓词、断言,等价于 Func<T,bool> 即返回bool的表达式
  2. Expression<TDelegate> 表达式树,这个类很关键,但是在这里会细说,我们会讲它的一个特殊的泛型类型:Expression<Func<T,bool>> 这个在某些数据源的查询中十分重要,它代表lambda表达式中一种特殊的表达式,即没有大括号和return关键字的那种。

我们先准备两个类

  1. Student/学生类:
/// <summary>
/// 学生
/// </summary>
public class Student
{
    /// <summary>
    /// 学号
    /// </summary>
    public int StudentId { get; set; }
    /// <summary>
    /// 姓名
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 班级
    /// </summary>
    public string Class { get; set; }
    /// <summary>
    /// 年龄
    /// </summary>
    public int Age { get; set; }
}
  1. Subject/科目类:

    /// <summary>
    /// 科目
    /// </summary>
    public class Subject
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 年级
        /// </summary>
        public string Grade { get; set; }
        /// <summary>
        /// 学号
        /// </summary>
        public int StudentId { get; set; }
        /// <summary>
        /// 成绩
        /// </summary>
        public int Score { get; set; }
    }
    

Subject 和Student通过学号字段一一关联,实际工作中数据表有可能会设计成这。

那么先虚拟两个数据源:IEnumerable<Student> studentsIEnumerable<Subject> subjects。先忽略这两个数据源的实际来源,因为在开发过程中数据来源有很多种情况,有数据库查询出来的结果、远程接口返回的结果、文件读取的结果等等。不过最后都会整理成IEnumerable<T>的子接口或实现类的对象。

常见方法介绍

Where 过滤数据,查询出符合条件的结果

where的方法声明:

public IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate)

可以看出不会转换数据类型,通过给定的lambda表达式或者一个方法进行过滤,获取返回true的元素。

示例:

// 获取年纪大于10但不大于12的同学们
List<Student> results = students.Where(t=>t.Age >10 && t.Age<= 12).ToList();

注意在调用ToList之后数据才会实质上查询出来。

Group 分组,依照指定内容进行分组

Group的方法声明有很多种:

最常用的一种是:

public static IEnumerable<System.Linq.IGrouping<TKey,TSource>> GroupBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);

示例:

//将学生按照班级进行分组
List<IGrouping<string,Student>> list = students.GroupBy(p => p.Class).ToList();

OrderBy/OrderByDescending 进行排序,按条件升序/降序

它们是一对方法,一个是升序一个降序,其声明是一样的:

常用的是:

public static System.Linq.IOrderedEnumerable<TSource> OrderBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);

示例:

//按年龄的升序排列:
List<Student> results = students.OrderBy(p => p.Age).ToList();
//按年龄的降序排列:
List<Student> results = students.OrderByDescending(p => p.Age).ToList();

First/Last 获取数据源的第一个/最后一个

这组方法有两个常用的重载声明:

First:

// 直接获取第一个
public static TSource First<TSource> (this IEnumerable<TSource> source);
// 获取满足条件的第一个
public static TSource First<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

Last:

// 直接获取最后一个
public static TSource Last<TSource> (this IEnumerable<TSource> source);
// 获取最后一个满足条件的元素
public static TSource Last<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> 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/All 是否存在/是否都满足

Any:是否存在元素满足条件

有两个版本,不过意思可能不太一样:

public static bool Any<TSource> (this IEnumerable<TSource> source);//数据源中是否有数据
//================
//是否存在满足条件的数据
public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

All :是否都满足条件:

public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> 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<TSource> Skip<TSource> (this IEnumerable<TSource> source, int count);

第二个:SkipLast,从尾巴开始略过几个元素,返回剩下的元素内容

public static IEnumerable<TSource> SkipLast<TSource> (this IEnumerable<TSource> source, int count);

第三个:SkipWhile,跳过满足条件的元素,返回剩下的元素

public static IEnumerable<TSource> SkipWhile<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

// 不保留前10个学生
List<Student> results = students.Skip(10).ToList();
// 不保留后10个学生
List<Student> results = students.SkipLast(10).ToList();
// 只要非一班的学生
List<Student> results = students.SkipWhere(p=>p.Class=="一班").ToList();
//上一行代码 等价于 = students.Where(p=>p.Class != "一班").ToList();

Take 选取几个元素

Take与Skip一样也有三个衍生方法,声明的参数类型也一样,这里就不对声明做介绍了,直接上示例。

//选取前10名同学
List<Student> results = students.Take(10).ToList();
// 选取最后10名同学
List<Student> results = students.TakeLast(10).ToList();
//选取 一班的学生
List<Student> results = students.TakeWhile(p=>p.Class=="一班").ToList();
// 上一行 等价于 = students.Where(p=>p.Class=="一班").ToList();

在使用Linq写分页的时候,就是联合使用Take和Skip这两个方法:

int pageSize = 10;//每页10条数据
int pageIndex = 1;//当前第一页
List<Student> results = students.Skip((pageIndex-1)*pageSize).Take(pageSize).ToList();

其中 pageIndex可以是任意大于0 的数字。Take和Skip比较有意思的地方就是,如果传入的数字比数据源的数据量大,根本不会爆粗,只会返回一个空数据源列表。

Select 选取

官方对于Select的解释是,将序列中的每个元素投影到新的表单里。我的理解就是,自己 定义一个数据源单个对象的转换器,然后按照自己的方式对数据进行处理,选择出一部分字段,转换一部分字段。

所以按我的理解,我没找到java8的同效果方法。(实际上java用的是map,所以没找到,😄)

public static System.Collections.Generic.IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector);

示例:

// 选出班级和姓名
List<object> results = students.Select(p => new
{
    p.Class,
    p.Name
}).ToList();

简单运算操作

Linq 里有几个需要注意的简单运算操作,这部分在使用中很常见。

Max 选取最大的一个

Max获取数据源中最大的一个,不过只能是数字类型的,其他类型因为不能直接比较大小所以可以有替代方法,就是先排序取第一个。

以下是Max方法的两个重载版本:

public static int Max (this IEnumerable<int> source);
public static int Max <TSource>(this IEnumerable<TSource> source,Func<TSource,int> 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<TSource> (this IEnumerable<TSource> source, TSource value);

//传入相等性比较器的
public static bool Contains<TSource> (this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> 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<Student>
{
    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<TSource> (this IEnumerable<TSource> source);

public static int Count<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);

示例:

int count = students.Count();//返回一共有多少个学生
int count = students.Count(p=>p.Class=="一班");// 统计一班一共有多少学生

同类型数据源的操作

之前介绍了单个数据源的操作方法,这些方法不会让数据源发生变化,更多的对数据源进行过滤和选择或者统计。现在介绍几个对多个数据源进行操作的方法。

Union 联合另一个同类型的数据源

联合另一个数据源,意思就是把两个数据源合并到一个里面,去掉重复的元素,只保留不重复的元素,并返回这个结果集。

与Contains方法差不多,这个方法有两个重载的版本:

public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);

public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

先假设一个业务场景:

学校举办运动会,现在教务处收到了田径组 500米跑的报名名单和跳远的报名名单,需要看看一共有哪些学生报名了这两项赛事。

// 省略数据源,田径组的名单
IEnumerable<Student> students1 = new List<Student>();
//省略数据源来源,跳远组的名单
IEnumerable<Student> students2 = new List<Student>();


List<Student> all = students1.Union(student2).ToList();

这时候简单统计了一下所有人,但是后来教务处在核对的时候,发现有的人名重复了,需要判断是否是一个人,这时候就必须创建一个相等比较器了。

List<Student> all = students1.Union(student2,compare).ToList();
// 省略compare的实现,具体可参照Contains的比较器

Intersect 获取两个集合中都存在的数据

获取同时存在于两个集合中的元素,与Union类似。

方法的声明如下:

public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);

public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

继续之前的业务场景,现在教务处需要知道有哪些同学同时报名了两个比赛

List<Student> students = students1.Intersect(students2).ToList();

Except 获取只在第一个数据源中存在的数据

获取只存在于第一个集合的元素,从第一个集合中去除同时存在与第二个集合的元素,并返回。

方法的声明如下:

public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);

public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

示例:

继续业务描述,教务处要一份只报名了500米的学生名单:

List<Student> students = students1.Except(students2).ToList();

Reverse 翻转顺序

数据源中的元素原本有一定的顺序,这个方法可以将数据源中的顺序翻转过来,原本是最后一个的变成了第一个

,第一个变成了最后一个。

简单示例:

char[] apple = { 'a', 'p', 'p', 'l', 'e' };

char[] reversed = apple.Reverse().ToArray();

Distinct 去重

对数据源进行去重,然后返回去重之后的结果。同样,这个方法有两个重载版本,一个有比较器,一个没有比较器。

// 不用比较器的
public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source);
// 设置比较器
public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);

示例:

先描述一个可能会出现的场景,每个班级在各个赛事组提交报名信息的时候有点混乱,500米的负责老师把一个班的名单多录了一次,但是学生已经乱序了,现在需要把多录的去掉,也就是对数据进行去重。

List<Student> students = students1.Distinct();

多个类型数据源的操作

之前的方法基本都是对一个类型的数据源进行操作,不会涉及其他类型的数据源。现在介绍一下怎么关联多个类型的数据源,类似于SQL里的多表链接查询。

Join 关联两个数据源

按照一定的逻辑将两个数据源关联到一起,然后选择出需要的数据。

方法有这几个重载版本:

public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector);

//
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector, IEqualityComparer<TKey> comparer);

这个方法的参数比较多,我们大概介绍一下这个方法的所有参数:

类型参数

  • TOuter 第一个序列中的元素的类型。

  • TInner 第二个序列中的元素的类型。

  • TKey 选择器函数返回的键的类型。

  • TResult 结果元素的类型。

参数

  • outer IEnumerable 要联接的第一个序列。

  • inner IEnumerable 要与第一个序列联接的序列。

  • outerKeySelector Func<TOuter,TKey> 用于从第一个序列的每个元素提取联接键的函数。

  • innerKeySelector Func<TInner,TKey> 用于从第二个序列的每个元素提取联接键的函数。

  • resultSelector Func<TOuter,TInner,TResult> 用于从两个匹配元素创建结果元素的函数。

  • comparerIEqualityComparer 用于对键进行哈希处理和比较的 IEqualityComparer

示例:

假设前天语文老师组织了一场考试,因为是模拟正式考试,所以答题纸上学生都只写了学号,现在需要把考试成绩和学生们联系在一起

List<object> 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<TResult> GroupJoin<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,IEnumerable<TInner>,TResult> resultSelector);
//设置比较器
public static IEnumerable<TResult> GroupJoin<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,IEnumerable<TInner>,TResult> resultSelector, IEqualityComparer<TKey> comparer);


类型参数

  • TOuter 第一个序列中的元素的类型。

  • TInner 第二个序列中的元素的类型。

  • TKey 键选择器函数返回的键的类型。

  • TResult 结果元素的类型。

参数

  • outer IEnumerable 要联接的第一个序列。

  • inner IEnumerable 要与第一个序列联接的序列。

  • outerKeySelector Func<TOuter,TKey> 用于从第一个序列的每个元素提取联接键的函数。

  • innerKeySelector Func<TInner,TKey> 用于从第二个序列的每个元素提取联接键的函数。

  • resultSelector Func<TOuter,IEnumerable ,TResult> 用于从第一个序列的元素和第二个序列的匹配元素集合中创建结果元素的函数。

  • 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<Person> people = new List<Person> { magnus, terry, charlotte };
    List<Pet> pets = new List<Pet> { 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的另一种查询方式的内容文章。

更多内容烦请关注我的博客

file

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!