LINQ 基础概念

标准查询运算符概述

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/linq/standard-query-operators-overview

Lambda 表达式

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/lambda-expressions

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

摘抄自:精通C# 5.0与.NET 4.5高级编程

LINQ 基础概念

什么是 LINQ

LINQ 是 Language Integrate Query 的缩写,它在对象和数据之间建立一种对应关系,可以使用访问内存对象的方式查询数据集合。

在.NET 类库中,LINQ 相关类库都在System.Linq命名空间下,该命名空间提供支持使用LINQ进行查询的类和接口,其中最主要的是以下两个类和两个接口。

  • � IEnumerable<T>接口:它表示可以查询的数据集合,一个查询通常是逐个对集合中的元素进行筛选操作,返回一个新的 IEnumerable<T>对象,用来保存查询结果。

  • � IQueryable<T>接口:它继承 IEnumerable<T>接口,表示一个可以查询的表达式目录树。

  • � Enumerable 类:它通过对 IEnumerbale<T>提供扩展方法,实现 LINQ 标准查询运 算符。包括过滤、导航、排序、查询、联接、求和、求最大值、求最小值等操作。

  • � Queryable 类:它通过对 IQueryable<T>提供扩展方法,实现 LINQ 标准查询运算符。包括过滤、导航、排序、查询、联接、求和、求最大值、求最小值等操作。

  • LINQ 查询的目的是从指定的数据源中查询满足符合特定条件的数据元素,并且根据需要对这些查询的元素进行排序、连接等操作。所以 LINQ 查询包括如下几个主要元素。

  • � 数据源:数据源表示 LINQ 查询将从哪里查找数据,它通常是一个或多个数据集,每 个数据集包含一系 列 的元素。数据集是一个类型为 IEnumerable<T>或

  • IQueryable<T>的对象,可以对它进行枚举,遍历每一个元素。此外,它的元素可以是任何数据类型,所以可以表示任何数据的集合。

  • � 目标数据:数据源中的元素并不定是查询所需要的结果。例如,对于一个学生信息集合中,查询 A 只是查询学生的姓名,查询 B 要查询学生的姓名和各科成绩,查询 C 则需要学生各科成绩的总分(需要另外计算),而不是原始数据中的各科成 绩。目标数据用来指定查询的具体想要的是什么数据。在 LINQ 中,它定义了查询结果数据集中元素的具体类型。

  • � 筛选条件:筛选条件定义了对数据源中元素的过滤条件。只有满足条件的元素才作为查询结果返回。筛选条件可以是简单的逻辑表达式表示,也可以用具有复杂逻辑的函数来表示。

  • � 附加操作:附加操作表示一些其他的具体操作。比如,对查询结果进行排序、计算查询结果的最值和求和、对查询结果进行分组等。

  • 其中,数据源和目标数据是 LINQ 查询的必备元素,筛选条件和附加操作是可选元素。

    查询表达式关键字

    指定要查找的数据源及范围变量,多个 from 子句则表示从多个数据源中查找数据

    select

    指定查询要返回的目标数据,可以指定任何类型,甚至是匿名类型

    where

    指定元素的筛选条件,多个 where 子句则表示并列条件,必须全部都满足才能入选

    orderby

    指定元素的排序字段和排序方式。当有多个排序字段时,由字段顺序确定主次关系,可指定升序和降序两种排序方式

    group

    指定元素的分组字段

    指定多个数据源的关联方式

    用 from 子句指定数据源

    每个 LINQ 查询都以 from 子句开始,from 子句包括以下两个功能。

  • � 指定查询将采用数据源。

  • � 定义一个本地变量,表示数据源中单个元素。

  • 用 select 子句指定目标数据

    LINQ 查询中,select 子句和 from 子句都是必备子句。 LINQ 查询表达式必须以 select或 group 子句结束。

    通常情况下,不需要为 select 子句中的元素指定具体数据类型。另外,如果查询结果中的元素只是在本函数内临时使用,尽量使用 匿名类型 ,这样可以减少很多不必要的类定义。

    用 where 子句指定筛选条件

    在 LINQ 中,用 where 子句指定查询的过滤条件

    在同一个 LINQ 查询中,还可以使用多个并列的 where 子句来进行多个条件过滤 。数据源中的元素只有同时满足所有 where 子句的条件才能作为查询结果。

    where 子句中的条件尽量简短易懂,并且还可以通过函数等方式来提供判断条件。当出现多个逻辑并(&&运算)的条件时,可以考虑使用多个并列的 where 子句代替。

    用 orderby 子句进行排序

    在 LINQ 中,通过 orderby 子句对查询结果进行排序操作。orderby 子句用来对查询结果进行升序或降序排序。

    升序(ascending)

    降序(desending)

    用 group 子句进行分组

    在 LINQ 中,用 group 子句实现对查询结果的分组操作。

    group element by key

    element 表示作为查询结果返回的元素,key 表示分组条件。group 子句返回类型为 IGrouping<TKey,TElement>的查询结果。其中,TKey 的类型为参数 key 的数据类型,TElement 的类型是参数 element 的数据类型。

    IGrouping<TKey,TElement>可以看成一个 HashTable 内部嵌套一个 List 列表的数据结果,它包括一个主要属性 Key。其类型为 TKey,表示查询中的分组关键字。通常使用两层 foreach 遍历 IGrouping 中的所有元素,外层 foreach 按照 Key 遍历,内层 foreach 按照对应的元素列表遍历。

    有时需要对分组的结果进行排序、再次查询等操作。这就需要 使用 into 关键字将 group查询的结果保存到一个临时变量,并且必须使用新的 select 或 group 子句对其进行重新查询 ,也可以使用 orderby 进行排序、用 where 进行过滤等操作。

    from 子句进行复合查询

    在 LINQ 中,有两种类型的 from 复合查询,第一种是对同一个数据源中的元素进行嵌套查询。在这种查询中,数据源的元素通常包含一个可以作为数据源的属性、方法等。外层 from 子句对数据源进行查询,内层 from 子句则对元素中的数据源进行查询。

    var query1 =

    from st in stAry

    from scr in st.Scores

    where scr.Score > 80

    group new { st.Name, scr } by st.Name;

    第二种类型的 from 复合子句是在多个数据源上进行查询。通常每个 from 子句都从一个数据源提取数据,通常还包含 where 子句对数据进行过滤。

    from val1 in intAry1

    from val2 in intAry2

    where val2 % val1 == 0

    group val2 by val1;

    上面多个数据源相当于组成一个笛卡尔积。

    用 join 子句进行联接

    用 join 子句进行内部联接

    在内部联接中 join 子句的格式如下。其中,dataSource 表示数据源,它是联接要使用的第二个数据集;element 表示存储 dataSource 中元素的本地变量;exp1 和 exp2 表示两个表达式,它们具有相同的数据类型,可以用 equals 进行比较。如果 exp1 和 exp2 相等,则当前的元素将添加到查询结果。

    join element in dataSource on exp1 equals exp2

    上面也是笛卡尔积 ,其中 on equals 不能省略,可以 on 1 equals 1来查找所有。

    用 join 子句进行分组联接

    有时需要将查询结果按照第一个数据集中的元素进行分组,这就需要使用 join 子句的另外一种用法——分组联接。分组联接的格式如下。其中,into 关键字表示将这些数据分组并保存到 grpName 中,grpName 是保存一组数据的集合。

    join element in dataSource on exp1 equals exp2 into grpName

    分组联接可用于产生分层的数据结果,它将第一个集合中的每个元素与第二个集合中的一组相关元素进行配对。值得注意的是, 即使第一个集合中的元素在第二个集合中没有配对元素,也会为它产生一个空的分组对象

    用 join 子句进行左外部联接

    第三种联接是左外部联接,它返回第一个集合中的所有元素,无论它是否在第二个集

    合中有相关元素。在 LINQ 中,通过对分组联接的结果调用 DefaultIfEmpty()方法来执行左

    外部联接。DefaultIfEmpty()方法从列表中获取指定元素。如果列表为空,则返回默认值。

    int[] intAry1 = { 5, 15, 23, 30, 33, 40 };

    //创建整数数组 intAry1 作为数据源

    int[] intAry2 = { 10, 20, 30, 50, 60, 70, 80 };

    //创建整数数组 intAry2 作为数据源

    //查询 query1 使用 join 子句从两个数据源获取数据

    //演示左联接的使用

    var query1 =

    from val1 in intAry1

    join val2 in intAry2 on val1 % 5 equals val2 % 15 into val2Grp

    from grp in val2Grp.DefaultIfEmpty()

    select new { VAL1 = val1, VAL2GRP = grp };

    foreach (var obj in query1) //打印查询 query1 的元素

    System.Console.WriteLine("{0}", obj);

    { VAL1 = 5, VAL2GRP = 30 }

    { VAL1 = 5, VAL2GRP = 60 }

    { VAL1 = 15, VAL2GRP = 30 }

    { VAL1 = 15, VAL2GRP = 60 }

    { VAL1 = 23, VAL2GRP = 0 }

    { VAL1 = 30, VAL2GRP = 30 }

    { VAL1 = 30, VAL2GRP = 60 }

    { VAL1 = 33, VAL2GRP = 0 }

    { VAL1 = 40, VAL2GRP = 30 }

    { VAL1 = 40, VAL2GRP = 60 }

    左外部联接和分组联接虽然相似但是并非一样。分组联接返回的查询结果是一种分层数据结构,需要使用两层 foreach 才能遍历它的结果。而左外部联接是在分组联接的查询结果上再进行一次查询,所以它在 join 之后还有一个 from 子句进行查询。

    LINQ 查询方法

    LINQ 中,数据源和查询结果实际上都是 IEnumerable<T>或 IQueryable<T>类型的对象,所以可以通过使用普通对象的形式(调用方法、使用属性等)对数据源进行查询并使用查询结果数据。

    IEnumerable<T>泛型接口支持在指定数据集合上进行迭代操作。它定义了一组扩展方法,用来对数据集合中的元素进行遍历、过滤、排序、搜索、定位等操作。在 LINQ 中,数据源实际上是实现了接口 IEnumerable<T>的类,通过 select 子句返回的查询结果也是一个实现了接口 IEnumerable<T>的类。.NET 类库中,IEnumerable<T>接口提供了大量与查询相关的方法。这些方法实际上是以扩展方法的形式定义,但是由于它的作用类型也为 IEnumerable<T>接口,所以使用上和成员方法很类似。如表 4.3 列出了 IEnumerable<T>接口的主要成员及其功能。

    IEnumerable<T>主要成员

    Aggregate

    对序列应用累加器函数,可以指定累加方法

    计算序列中所有元素的和,返回值有 int、long、float、double、decimal 类型,并且可以指定元素到数值的映射方法

    Average

    计算序列中所有元素的平均值,返回值有 int、long、float、double、decimal 类型,并且可以指定元素到数值的映射方法

    计算序列中所有元素的最大值,返回值有 int、long、float、double、decimal 类型,并且可以指定元素到数值的映射方法

    计算序列中所有元素的最小值,返回值有 int、long、float、double、decimal 类型,并且可以指定元素到数值的映射方法

    检查是否序列中所有元素都满足条件,可以指定条件判断方法。如果所有元素都满足条件返回 TRUE,否则返回 FALSE

    检查序列中是否有任何一个元素满足条件,可以指定条件判断方法。如果有一个以上(含一个)元素满足条件返回 TRUE,否则返回 FALSE

    Contains

    检查数据序列中是否包含特定的元素,可以指定相等比较方法

    Count

    返回序列中满足指定条件的元素的数量,可以指定条件判断方法

    LongCount

    返回序列中满足指定条件的元素的长数量,可以指定条件判断方法

    将 IEnumerable 中的元素转换为指定的数据类型

    DefaultIfEmpty

    返回序列中指定位置的元素。如果序列为空,则返回默认的元素值

    ElementAt

    返回序列中指定索引处的元素

    ElementAtOrDefault

    返回序列中指定索引处的元素。如果索引超出范围,则返回默认值

    First

    返回序列中满足指定条件的第一个元素,可以指定条件判断方法

    FirstOrDefault

    返回序列中满足指定条件的第一个元素。如果不存在则返回默认值,也可以指定条件判断方法

    返回序列中满足指定条件的最后一个元素,可以指定条件判断方法

    LastOrDefault

    返回序列中满足指定条件的最后一个元素。如果不存在则返回默认值,也可以指定条件判断方法

    Single

    返回序列中满足指定条件的唯一元素。如果不止一个元素满足条件会引发异常,可以指定条件判断方法

    SingleOrDefault

    返回序列中满足指定条件的唯一元素。如果不存在则返回默认值,如果不止一个元素满足条件会引发异常,可以指定条件判断方法

    Reverse

    反转序列中元素的顺序

    Distinct

    返回序列中不重复的元素的集合,可以指定相等比较方法

    Concat

    连接两个序列,直接首尾相连。返回结果可能存在重复数据

    Except

    获取两个元素集合的差集,可以指定相等比较方法

    Intersect

    获取两个元素集合的交集,可以指定相等比较方法

    Union

    获取两个元素集合的并集,可以指定相等比较方法

    SequenceEqual

    比较两个序列是否相等,可以指定相等比较方法

    Where

    根据指定条件对集合中元素进行筛选,返回满足条件的元素集合

    跳过序列中指定数量的元素,然后返回剩余的元素

    SkipWhile

    跳过序列中满足指定条件的元素,然后返回剩余的元素,可以指定条件判断方法

    从序列的开头返回指定数量的连续元素

    TakeWhile

    返回从序列开始的满足指定条件的连续元素,可以指定条件判断方法

    ToArray

    从 IEnumerable<T>创建一个数组

    ToList

    从 IEnumerable<T>创建一个 List<T>

    IEnuerable<T>继承至 IEnumerable 接口,因此它也包含 IEnumerable 接口的所有方法,即还包括 Select()、SelectMany()、Repeat()等方法

    另外,IQueryable<T>接口从 IEnumerable<T>派生而来,通常也可以作为数据源使用,它的使用和 IEnumerable<T>类似。

    Lambda 表达式

    LINQ 中,所有的查询操作实际上都是在一个 IEnumerable<T>类型的对象上进行操作。所以,LINQ 表达式在真正执行时都转换成具体的函数调用。这就要求在LINQ 查询关键字(from、select 等)和 IEnumerable<T>接口的方法之间有一个对应关系。

    Lambda 表达式实际是匿名函数,它可以赋值到一个委托,而在 IEnumerable<T>的方法中很多都通过函数委托来实现自定义的运算、条件等操作,所以 Lambda 表达式在LINQ 中被广泛使用。

    用 Where()方法进行筛选

    在 LINQ 查询中,where 子句可以用 IEnumerable<T>.Where()方法来实现。该方法接收一个函数委托作为参数,该委托指定过滤的具体实现,返回符合条件的元素集合。包括两个版本的 Where()方法。

  • 只对数据集合中的元素进行过滤

  • 同时对数据集合中的元素和索引进行过滤

  • 用 OrderBy()方法进行排序

    在LINQ中,可以使用OrderBy()方法从小到大排序元素,也可以用OrderByDescending()

    方法从大到小排序元素。

    这两个方法各自包含两个版本

    用 Skip()、SkipWhile()跳过元素

    在一些查询实例中,明确知道要跳过某些元素,只提取剩下的元素作为查询结果,这 就需要使用 IEnumerable<T>接口的 Skip()或 SkipWhile()两个方法。它们都是用来跳过集合中的元素,Skip()只是简单的跳过集合中指定数量的元素,而 SkipWhile()则跳过集合中满足指定条件的元素。

    SkipWhile()从集合中第 1 个元素开始,使用参数 predicate 进行计算。如果返回 True,则跳过并继续判断下一个元素。如果 predicate 返回 False,则停止判断,返回集合中没有被跳过的所有元素。

    SkipWhile()不是跳过所有的集合

    Take()、TakeWhile()提取元素

    Skip()相反,IEnumerable<T>还提供了 Take()和 TakeWhile()方法。

    TakeWhile()不是提取所有的集合

    Max()等对元素进行数值计算

    在传统的 SQL 查询语言中,还包括对集合中字段的数值运算操作,包括求最大值、求

    最小值、求平均值和求和。同样,在 IEnumerable<T>也提供了以下等价方法完成这些操作。

  • � Min():计算集合中指定元素的最小值。

  • � Max():计算集合中指定元素的最大值。

  • � Sum():计算集合中指定元素的累加和。

  • � Average():计算集合中指定元素的平均值。

  • 这些数值计算函数,都包括 13 个重载版本,最简单的一个版本不接收任何参数,此

    时参与计算的元素类型必须具有默认的数值运算支持。其中,Max()和 Min()都需要类型具

    有排序功能,这些类型包括 int 和 float 等数值类型、string 字符串类型和枚举类型等。而

    Sum()和 Average()两个方法,则需要目标类型能够默认转化成 int 和 float 等数值类型,从

    而进行累加和平均计算。

    而字符串大小比较则是按照字母表的顺序进行比较的。

    由于 string 类型并没有提供直接转化到数值类型(int、float 等)的方法,所以不能对 strAry 进行求和或平均操作,即代码 strAry.Sum()是错误的语法。

    实际上,在很多开发应用中,需要对非数值类型的数据进行求和、求平均、求最大值和 求最小值等数值操作,这就需要使用数值操作的重载版本。在这些重载版本中,需要接收一个函数委托类型的参数。该委托将特定类型的数据转换成数值类型,从而进行累加等操作。

    用 Distinct()消除集合中相等的元素

    在 LINQ 中,可以通过 IEnumerable<T>提供的 Distinct()方法完成。Distinct()方法包括一个不带参数的版本,

    它使用默认的相等比较器对集合中的元素进行比较

    当元素不是 int 之类的简单类型,甚至该类型根本不存在默认的相等比较器时,要对集合进行消除重复操作就需要使用 Distinct()的另外一个版本。该版本需要指定使用者提供一个相等比较器。Distinct()方法通过该比较器进行元素重复判断

    IEqualityComparer<T>接口包括两个成员方法,即 Equals()方法和 GetHashCode()方法

    在编写自定义的相等比较器时,将 HashCode 和 Equals()看成是两个具有主次关系的条件进行区分,前者为主,后者为次。这样可以使比较器的设计和实现变得更加清晰,功能更明确。

    用 Concat()连接两个集合

    LINQ 中,还可以通过 IEnumerable<T>的 Concat()方法将两个集合中的元素首尾相

    连,从而构成一个新的 IEnumerable<T>对象

    必须两个数据集合中的元素是相同类型,否则不能进行连接操作。

    Concat()方法是直接将两个集合中的元素连接在一起,不会进行重新排序、过滤等,就算两个集合中元素有重复也同样保留。

    用 Union()等进行集合操作

    并集、交集和差集 3 个常用操作,在 LINQ 中,IEnumerable<T>类分别通过 Union()、Intersect()和 Except()方法完成这 3 个操作。这 3 个方法各自都包含两个重载版本,其中一个版本不需要自定义相等比较器参数,它是默认相等比较器进行元素相等比较

  • Union():该方法对集合 A 和集合 B 进行并集操作,返回两个集合中的所有元素,相等的元素只出现一次。