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
方法,还会看到许多其他方法(如 Select
、SelectMany
、Join
和 Orderby
)。 这些方法实现标准查询运算符。
虽然看起来好像 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
生成筛选序列,然后 Orderby
对 Where
所生成的序列进行排序。 由于查询返回 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
语句或其他语句中循环访问查询变量之前,查询不会实际执行。
示例 - 方法语法
某些查询操作必须表示为方法调用。 最常见的此类方法是可返回单一数值的方法,例如 Sum、Max、Min、Average 等。 这些方法在任何查询中都必须始终最后一个调用,因为它们返回单个值,不能用作额外查询操作的源。 下面的示例演示查询表达式中的方法调用:
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.Action 或 System.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);
在运行时动态指定谓词筛选器
在某些情况下,在运行时之前你不知道必须将多少个谓词应用于 where
子句中的源元素。 动态指定多个谓词筛选器的方法之一是使用 Contains 方法,如以下示例中所示。 查询将根据执行查询时的 id
值返回不同的结果。
int[] ids = [111, 114, 112];
var queryNames =
from student in students
where ids.Contains(student.ID)
select new
student.LastName,
student.ID
foreach (var name in queryNames)
Console.WriteLine($"{name.LastName}: {name.ID}");
/* Output:
Garcia: 114
O'Donnell: 112
Omelchenko: 111
// Change the ids.
ids = [122, 117, 120, 115];
// The query will now return different results
foreach (var name in queryNames)
Console.WriteLine($"{name.LastName}: {name.ID}");
/* Output:
Adams: 120
Feng: 117
Garcia: 115
Tucker: 122
可以使用控制流语句(如 if... else
或 switch
)在预确定的替代查询之间进行选择。 在下面的示例中,studentQuery
使用其他 where
子句,如果 oddYear
的运行时值为 true
或 false
。
void FilterByYearType(bool oddYear)
IEnumerable<Student> studentQuery = oddYear
? (from student in students
where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
select student)
: (from student in students
where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
select student);
var descr = oddYear ? "odd" : "even";
Console.WriteLine($"The following students are at an {descr} year level:");
foreach (Student name in studentQuery)
Console.WriteLine($"{name.LastName}: {name.ID}");
FilterByYearType(true);
/* Output:
The following students are at an odd year level:
Fakhouri: 116
Feng: 117
Garcia: 115
Mortensen: 113
Tucker: 119
Tucker: 122
FilterByYearType(false);
/* Output:
The following students are at an even year level:
Adams: 120
Garcia: 114
Garcia: 118
O'Donnell: 112
Omelchenko: 111
Zabokritski: 121
在查询表达式中处理 null 值
此示例显示如何在源集合中处理可能的 null 值。 IEnumerable<T> 等对象集合可包含值为 null 的元素。 如果源集合为 null
或包含值为 null
的元素,并且查询不处理 null
值,则在执行查询时将引发 NullReferenceException。
可采用防御方式进行编码,以避免空引用异常,如以下示例所示:
var query1 =
from c in categories
where c != null
join p in products on c.ID equals p?.CategoryID
select new
Category = c.Name,
Name = p.Name
在前面的示例中,where
子句筛选出类别序列中的所有 null 元素。 此方法独立于 join 子句中的 null 检查。 在此示例中,带有 null 的条件表达式有效,因为 Products.CategoryID
的类型为 int?
,这是 Nullable<int>
的速记形式。
在 join 子句中,如果只有一个比较键是可以为 null 的值类型,则可以在查询表达式中将另一个比较键转换为可以为 null 的值类型。 在以下示例中,假定 EmployeeID
是包含 int?
类型的值列:
var query =
from o in db.Orders
join e in db.Employees
on o.EmployeeID equals (int?)e.EmployeeID
select new { o.OrderID, e.FirstName };
每个示例中都使用 equals
查询关键字。 还可使用模式匹配,其中包括 is null
和 is not null
的模式。 不建议在 LINQ 查询中使用这些模式,因为查询提供程序可能无法正确解读新的 C# 语法。 查询提供程序是一个库,用于将 C# 查询表达式转换为本机数据格式,例如 Entity Framework Core。 查询提供程序实现 System.Linq.IQueryProvider 接口,以创建实现 System.Linq.IQueryable<T> 接口的数据源。
在查询表达式中处理异常
在查询表达式的上下文中可以调用任何方法。 请勿在查询表达式中调用任何会产生副作用(如修改数据源内容或引发异常)的方法。 此示例演示在查询表达式中调用方法时如何避免引发异常,而不违反有关异常处理的常规 .NET 指南。 这些指南阐明,当你理解在给定上下文中为何会引发异常时,捕获到该特定异常是可以接受的。 有关详细信息,请参阅异常的最佳做法。
最后的示例演示了在执行查询期间必须引发异常时,该如何处理这种情况。
以下示例演示如何将异常处理代码移到查询表达式外。 只有当方法不取决于查询的任何本地变量时,才可以执行重构。 在查询表达式之外处理异常更容易。
// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();
// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
dataSource = GetData();
catch (InvalidOperationException)
Console.WriteLine("Invalid operation");
if (dataSource is not null)
// If we get here, it is safe to proceed.
var query =
from i in dataSource
select i * i;
foreach (var i in query)
Console.WriteLine(i.ToString());
在上述示例的 catch (InvalidOperationException)
块中,请以适合你的应用程序的方式处理(或不处理)异常。
在某些情况下,针对由查询内部引发的异常的最佳措施可能是立即停止执行查询。 下面的示例演示如何处理可能在查询正文内部引发的异常。 假定 SomeMethodThatMightThrow
可能导致要求停止执行查询的异常。
try
块封装 foreach
循环,且不对自身进行查询。 foreach
循环是实际执行查询时的点。 执行查询时,会引发运行时异常。 因此,必须在 foreach
循环中处理这些异常。
// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
s[4] == 'C' ?
throw new InvalidOperationException() :
@"C:\newFolder\" + s;
// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];
// Demonstration query that throws.
var exceptionDemoQuery =
from file in files
let n = SomeMethodThatMightThrow(file)
select n;
foreach (var item in exceptionDemoQuery)
Console.WriteLine($"Processing {item}");
catch (InvalidOperationException e)
Console.WriteLine(e.Message);
/* Output:
Processing C:\newFolder\fileA.txt
Processing C:\newFolder\fileB.txt
Operation is not valid due to the current state of the object.
请记得捕获预期引发的任何异常,并且/或者在 finally
块中执行任何必要的清理。
演练:用 C# 编写查询
where 子句
基于运行时状态进行查询
Nullable<T>
可以为 null 的值类型
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:https://aka.ms/ContentUserFeedback。
提交和查看相关反馈