编写 C# LINQ 查询以查询数据

介绍性的语言集成查询 (LINQ) 文档中的大多数查询是使用 LINQ 声明性查询语法编写的。 但是在编译代码时,查询语法必须转换为针对 .NET 公共语言运行时 (CLR) 的方法调用。 这些方法调用会调用标准查询运算符(名称为 WhereSelectGroupByJoinMaxAverage 等)。 可以使用方法语法(而不查询语法)来直接调用它们。

查询语法和方法语法在语义上是相同的,但是查询语法通常更简单且更易于阅读。 某些查询必须表示为方法调用。 例如,必须使用方法调用表示检索与指定条件匹配的元素数的查询。 还必须对检索源序列中具有最大值的元素的查询使用方法调用。 System.Linq 命名空间中的标准查询运算符的参考文档通常使用方法语法。 你应该熟悉如何在查询和查询表达式本身中使用方法语法。

标准查询运算符扩展方法

下面的示例演示一个简单查询表达式以及编写为基于方法的查询的语义上等效的查询。

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

这两个示例的输出是相同的。 可以看到查询变量的类型在两种形式中是相同的:IEnumerable<T>

为了了解基于方法的查询,我们来仔细讨论它。 在表达式右侧,请注意,where 子句现在表示为 numbers 对象上的实例方法,它具有类型 IEnumerable<int>。 如果熟悉泛型 IEnumerable<T> 接口,则会知道它没有 Where 方法。 但是,如果在 Visual Studio IDE 中调用 IntelliSense 完成列表,则不仅会看到 Where 方法,还会看到许多其他方法(如 SelectSelectManyJoinOrderby)。 这些方法实现标准查询运算符。

Screenshot showing all the standard query operators in Intellisense.

虽然看起来好像 IEnumerable<T> 包括其他方法,但它没有。 标准查询运算符作为扩展方法来实现。 扩展方法可“扩展”现有类型;它们可以如同类型上的实例方法一样进行调用。 标准查询运算符扩展了 IEnumerable<T>,因此可以写入 numbers.Where(...)

若要使用扩展方法,请使用 using 指令将它们引入范围。 从应用程序的角度来看,扩展方法与常规实例方法是相同的。

有关扩展方法的详细信息,请参阅扩展方法。 有关标准查询运算符的详细信息,请参阅标准查询运算符概述 (C#)。 某些 LINQ 提供程序(如 实体框架和 LINQ to XML),会实现自己的标准查询运算符,并为 IEnumerable<T> 之外的其他类型实现扩展方法。

Lambda 表达式

在上面的示例中,请注意,条件表达式 (num % 2 == 0) 作为内联参数传递给 Enumerable.Where 方法:Where(num => num % 2 == 0). 此内联表达式为 lambda 表达式。 编写代码是一种方便的方法,否则必须以更繁琐的形式编写代码。 运算符左侧的 num 是输入变量,它与查询表达式中的 num 对应。 编译器可以推断出 num 的类型,因为它知道 numbers 是泛型 IEnumerable<T> 类型。 Lambda 的主体与查询语法中或任何其他 C# 表达式或语句中的表达式完全相同。 它可以包含方法调用和其他复杂逻辑。 返回值就是表达式结果。 某些查询只能采用方法语法进行表示,而其中一些查询需要 lambda 表达式。 Lambda 表达式是 LINQ 工具箱中的一个强大且灵活的工具。

查询的可组合性

在前面的代码示例中,Enumerable.OrderBy 方法通过对 Where 调用使用点运算符来调用。 Where 生成筛选序列,然后 OrderbyWhere 所生成的序列进行排序。 由于查询返回 IEnumerable,因此可通过将方法调用链接在一起在方法语法中撰写查询。 使用查询语法编写查询时,编译器会执行此组合。 因为查询变量不存储查询的结果,所以可以随时修改它或将它用作新查询的基础(即使在执行它之后)。

下面的示例演示使用前面列出的每种方法的一些简单 LINQ 查询。

注意

这些查询对简单的内存中集合进行操作;但是,基本语法等同于在 LINQ to Entities 和 LINQ to XML 中使用的语法。

示例 - 查询语法

使用查询语法编写大多数查询来创建查询表达式。 下面的示例演示三个查询表达式。 第一个查询表达式演示如何通过应用包含 where 子句的条件来筛选或限制结果。 它返回源序列中值大于 7 或小于 3 的所有元素。 第二个表达式演示如何对返回的结果进行排序。 第三个表达式演示如何根据某个键对结果进行分组。 此查询基于单词的第一个字母返回两个组。

List<int> numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

查询的类型为 IEnumerable<T>。 可以使用 var 编写所有这些查询,如下面的示例所示:

var query = from num in numbers...

在前面的每个示例中,在 foreach 语句或其他语句中循环访问查询变量之前,查询不会实际执行。

示例 - 方法语法

某些查询操作必须表示为方法调用。 最常见的此类方法是可返回单一数值的方法,例如 SumMaxMinAverage 等。 这些方法在任何查询中都必须始终最后一个调用,因为它们返回单个值,不能用作额外查询操作的源。 下面的示例演示查询表达式中的方法调用:

List<int> numbers1 = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
List<int> numbers2 = [15, 14, 11, 13, 19, 18, 16, 17, 12, 10];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

如果方法具有 System.ActionSystem.Func<TResult> 参数,则这些参数以 lambda 表达式的形式提供,如下面的示例所示:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

在前面的查询中,只有查询 #4 立即执行,因为它返回单个值,而不是泛型 IEnumerable<T> 集合。 方法本身使用 foreach 或类似的代码来计算其值。

上面的每个查询可以通过 `var`` 使用隐式类型化进行编写,如下面的示例所示:

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

示例 - 混合查询和方法语法

此示例演示如何对查询子句的结果使用方法语法。 只需将查询表达式括在括号中,然后应用点运算符并调用方法。 在下面的示例中,查询 #7 返回对值介于 3 与 7 之间的数字进行的计数。 但是通常情况下,最好使用另一个变量存储方法调用的结果。 采用此方法时,查询不太可能与查询的结果相混淆。

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

由于查询 #7 返回单个值而不是集合,因此查询立即执行。

前面的查询可以通过 var 使用隐式类型化进行编写,如下所示:

var numCount = (from num in numbers...

它可以采用方法语法进行编写,如下所示:

var numCount = numbers.Count(n => n is > 3 and < 7);

它可以使用显式类型化进行编写,如下所示:

int numCount = numbers.Count(n => n is > 3 and < 7);

另请参阅