介绍性的语言集成查询 (LINQ) 文档中的大多数查询是使用 LINQ 声明性查询语法编写的。 但是在编译代码时,查询语法必须转换为针对 .NET 公共语言运行时 (CLR) 的方法调用。 这些方法调用会调用标准查询运算符(名称为
Where
、
Select
、
GroupBy
、
Join
、
Max
和
Average
等)。 可以使用方法语法(而不查询语法)来直接调用它们。
查询语法和方法语法在语义上是相同的,但是许多人发现查询语法更简单且更易于阅读。 某些查询必须表示为方法调用。 例如,必须使用方法调用表示检索与指定条件匹配的元素数的查询。 还必须对检索源序列中具有最大值的元素的查询使用方法调用。
System.Linq
命名空间中的标准查询运算符的参考文档通常使用方法语法。 因此,即使在开始编写 LINQ 查询时,熟悉如何在查询和查询表达式本身中使用方法语法也十分有用。
标准查询运算符扩展方法
下面的示例演示一个简单查询表达式以及编写为基于方法的查询的语义上等效的查询。
class QueryVMethodSyntax
static void Main()
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 + " ");
// Keep the console open in debug mode.
Console.WriteLine(System.Environment.NewLine);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
Output:
6 8 10 12
6 8 10 12
这两个示例的输出是相同的。 可以看到查询变量的类型在两种形式中是相同的:IEnumerable<T>。
为了了解基于方法的查询,我们来仔细讨论它。 在表达式右侧,请注意,where
子句现在表示为 numbers
对象上的实例方法,它具有类型 IEnumerable<int>
(如同你会回忆起的那样)。 如果熟悉泛型 IEnumerable<T> 接口,则会知道它没有 Where
方法。 但是,如果在 Visual Studio IDE 中调用 IntelliSense 完成列表,则不仅会看到 Where
方法,还会看到许多其他方法(如 Select
、SelectMany
、Join
和 Orderby
)。 这些都是标准查询运算符。
虽然看起来似乎 IEnumerable<T> 进行了重新定义以包括这些其他方法,不过实际上情况并非如此。 标准查询运算符作为一种新类型的方法(称为扩展方法)来实现。 扩展方法可“扩展”现有类型;它们可以如同类型上的实例方法一样进行调用。 标准查询运算符扩展了 IEnumerable<T>,因此可以写入 numbers.Where(...)
。
若要开始使用 LINQ,你在扩展方法方面实际需要了解的所有内容是如何使用正确的 using
指令将它们引入应用程序的范围。 从应用程序的角度来看,扩展方法与常规实例方法是相同的。
有关扩展方法的详细信息,请参阅扩展方法。 有关标准查询运算符的详细信息,请参阅标准查询运算符概述 (C#)。 某些 LINQ 提供程序(如 LINQ to SQL 和 LINQ to XML),会实现自己的标准查询运算符,并为 IEnumerable<T> 之外的其他类型实现额外的扩展方法。
Lambda 表达式
在上面的示例中,请注意,条件表达式 (num % 2 == 0
) 作为内联参数传递给 Where
方法:Where(num => num % 2 == 0).
此内联表达式称为 lambda 表达式。 可采用匿名方法、泛型委托或表达式树的形式编写原本必须以更繁琐的形式编写的代码,这是一种便利的方式。 在 C# 中,=>
是 lambda 运算符(读为“转到”)。 运算符左侧的 num
是输入变量,它与查询表达式中的 num
对应。 编译器可以推断出 num
的类型,因为它知道 numbers
是泛型 IEnumerable<T> 类型。 Lambda 的主体与查询语法中或任何其他 C# 表达式或语句中的表达式完全相同;它可以包含方法调用和其他复杂逻辑。 “返回值”就是表达式结果。
若要开始使用 LINQ,不必大量使用 lambda。 但是,某些查询只能采用方法语法进行表示,而其中一些查询需要 lambda 表达式。 进一步熟悉 lambda 之后,你会发现它们是 LINQ 工具箱中一种强大而灵活的工具。 有关详细信息,请参阅 Lambda 表达式。
查询的可组合性
在前面的代码示例中,请注意,OrderBy
方法通过对 Where
调用使用点运算符来调用。 Where
会生成经过筛选的序列,然后 Orderby
通过进行排序来对该序列进行操作。 由于查询返回 IEnumerable
,因此可通过将方法调用链接在一起在方法语法中撰写查询。 这是当你使用查询语法编写查询时,编译器在幕后进行的工作。 因为查询变量不存储查询的结果,所以可以随时修改它或将它用作新查询的基础(即使在执行过它之后)。
下面的示例演示使用前面列出的每种方法的一些简单 LINQ 查询。 一般情况下,规则是尽可能使用 (1),每当需要时使用 (2) 和 (3)。
这些查询对简单的内存中集合进行操作;但是,基本语法等同于在 LINQ to Entities 和 LINQ to XML 中使用的语法。
示例 - 查询语法
编写大多数查询的推荐方式是使用查询语法创建查询表达式。 下面的示例演示三个查询表达式。 第一个查询表达式演示如何通过应用包含 where
子句的条件来筛选或限制结果。 它返回源序列中值大于 7 或小于 3 的所有元素。 第二个表达式演示如何对返回的结果进行排序。 第三个表达式演示如何根据某个键对结果进行分组。 此查询基于单词的第一个字母返回两个组。
List<int> numbers = new() { 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 < 3 || num > 7
select num;
// Query #2.
IEnumerable<int> orderingQuery =
from num in numbers
where num < 3 || num > 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
语句或其他语句中循环访问查询变量之前,查询不会实际执行。 有关详细信息,请参阅 LINQ 查询介绍。
示例 - 方法语法
某些查询操作必须表示为方法调用。 最常见的此类方法是可返回单一数值的方法,例如 Sum、Max、Min、Average 等。 这些方法在任何查询中都必须始终最后一个调用,因为它们只表示单个值,不能用作其他查询操作的源。 下面的示例演示查询表达式中的方法调用:
List<int> numbers1 = new() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
List<int> numbers2 = new() { 15, 14, 11, 13, 19, 18, 16, 17, 12, 10 };
// Query #4.
double average = numbers1.Average();
// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);
如果方法具有 Action 或 Func 参数,则这些参数以 lambda 表达式的形式提供,如下面的示例所示:
// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);
在上面的查询中,只有查询 #4 立即执行。 这是因为它将返回单个值,而不是泛型 IEnumerable<T> 集合。 该方法本身必须使用 foreach
才能计算其值。
上面的每个查询可以通过 var 使用隐式类型化进行编写,如下面的示例所示:
// var is used for convenience in these queries
var 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
int numCount1 = (
from num in numbers1
where num < 3 || num > 7
select num
).Count();
// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
from num in numbers1
where num < 3 || num > 7
select num;
int numCount2 = numbersQuery.Count();
由于查询 #7 返回单个值而不是集合,因此查询立即执行。
前面的查询可以通过 var
使用隐式类型化进行编写,如下所示:
var numCount = (from num in numbers...
它可以采用方法语法进行编写,如下所示:
var numCount = numbers.Where(n => n < 3 || n > 7).Count();
它可以使用显式类型化进行编写,如下所示:
int numCount = numbers.Where(n => n < 3 || n > 7).Count();
演练:用 C# 编写查询
语言集成查询 (LINQ)
where 子句