摘自msdn (
http://msdn.microsoft.com/zh-cn/magazine/cc337893.aspx)
LINQ 的标准查询操作符
LINQ 的标准查询操作符
John Papa
代码下载位置: DataPoints2008_03.exe (958 KB)
Browse the Code Online
语言集成查询 (LINQ) 允许开发人员通过强类型化语法使用 Microsoft
® .NET Framework 3.5 代码编写类似 SQL 的查询。然后,各种 LINQ 提供程序,如 LINQ to Objects(可利用它根据对象层次结构编写查询)和 LINQ to Entities(可利用它根据实体框架的概念模型编写查询)可根据代表数据存储的细微差别来有效处理这些查询。
除强类型化语法外,LINQ 查询还具有一个标准查询操作符库来增强其功能。这些标准查询操作符对序列进行运算并可执行各种运算,如确定序列中是否存在某个值以及对序列运行合计函数(如求和)。
在本月的专栏中,我将使用 LINQ 来执行实际的查询和运算(会用到 LINQ to Objects 和 LINQ to Entities)。我将查询一个实体集合并使用其导航属性深入研究一组具备层次结构的实体。我还会为您演示如何对数组和集合应用多个标准查询操作符。并展示如何使用 lambda 表达式强化 LINQ 的标准查询操作符,以及如何利用它们来从序列解析特定信息并对序列执行复杂的逻辑运算。本专栏的下载中提供有所有代码示例(请参见
msdn.microsoft.com/msdnmag/code08.aspx)。
操作符和 LINQ
LINQ 自身功能非常强大,无论使用的是 LINQ to XML、LINQ to DataSets、LINQ to Entities、LINQ to Objects 还是附带的任何其他 LINQ 提供程序。LINQ 的核心功能在于其强类型化查询语法,它可用于任意此类提供程序。当将 LINQ 与一个或多个标准查询操作符结合使用时,会得到一个功能更为强大的工具集,从而可精细地控制一组数据。
标准查询操作符在 System.Linq 命名空间中的 System.Core.dll 程序集中作为静态类 Enumerable 和 Queryable 的扩展方法存在,并且可用于实现 IEnumerable<T> 或 IQueryable<T> 的对象。这样它们就能使用 LINQ to Entities 和 LINQ to SQL 之类的提供程序对各类对象执行运算,从内存中的集合和数组(序列)到远程数据库。
可轻松地确定处理特定任务时所拥有的操作符。如果要在 LINQ 查询中使用操作符,可使用 Queryable 静态类可用扩展方法中的操作符。如果要对实现 IEnumerable<T> 的序列使用操作符,可使用 Enumerable 静态类中的一个扩展方法。但是,请记住:并非 Queryable 类中的所有操作符都适用于基础数据存储,因此运行时可能不支持某些操作符。
操作符类型
操作符有多种类型(使用对象浏览器查看 Enumerable 和 Queryable 类即可找到所有操作符)。
图 A 以字母顺序显示了不同类型操作符的分类。可利用它来大致了解一下操作符所提供的功能。我将使用 LINQ to Objects 和 LINQ to Entities 展示一小组此类操作符,以显示它们如何为实际应用程序带来好处。
Figure A Categories of Operators
操作符 | 说明 |
---|---|
聚合 | |
Aggregate | 对序列执行一个自定义方法 |
Average | 计算数值序列的平均值 |
Count | 返回序列中的项目数(整数) |
LongCount | 返回序列中的项目数(长型) |
Min | 查找数字序列中的最小数 |
Max | 查找数字序列中的最大数 |
Sum | 汇总序列中的数字 |
连接 | |
Concat | 将两个序列连成一个序列 |
转换 | |
Cast | 将序列中的元素转换成指定类型 |
OfType | 筛选序列中指定类型的元素 |
ToArray | 从序列返回一个数组 |
ToDictionary | 从序列返回一个字典 |
ToList | 从序列返回一个列表 |
ToLookup | 从序列返回一个查询 |
ToSequence | 返回一个 IEnumerable 序列 |
元素 | |
DefaultIfEmpty | 为空序列创建默认元素 |
ElementAt | 返回序列中指定索引的元素 |
ElementAtOrDefault | 返回序列中指定索引的元素,或者如果索引超出范围,则返回默认值 |
First | 返回序列中的第一个元素 |
FirstOrDefault | 返回序列中的第一个元素,或者如果未找到元素,则返回默认值 |
Last | 返回序列中的最后一个元素 |
LastOrDefault | 返回序列中的最后一个元素,或者如果未找到元素,则返回默认值 |
Single | 返回序列中的单个元素 |
SingleOrDefault | 返回序列中的单个元素,或者如果未找到元素,则返回默认值 |
相等 | |
SequenceEqual | 比较两个序列看其是否相等 |
生成 | |
Empty | 生成一个空序列 |
Range | 生成一个指定范围的序列 |
Repeat | 通过将某个项目重复指定次数来生成一个序列 |
分组 | |
GroupBy | 按指定分组方法对序列中的项目进行分组 |
联接 | |
GroupJoin | 通过归组将两个序列联接在一起 |
Join | 将两个序列从内部联接起来 |
排序 | |
OrderBy | 以升序按值排列序列 |
OrderByDescending | 以降序按值排列序列 |
ThenBy | 升序排列已排序的序列 |
ThenByDescending | 降序排列已排序的序列 |
Reverse | 颠倒序列中项目的顺序 |
分区 | |
Skip | 返回跳过指定数目项目的序列 |
SkipWhile | 返回跳过不满足表达式项目的序列 |
Take | 返回具有指定数目项目的序列 |
TakeWhile | 返回具有满足表达式项目的序列 |
投影 | |
Select | 创建部分序列的投影 |
SelectMany | 创建部分序列的一对多投影 |
限定符 | |
All | 确定序列中的所有项目是否满足某个条件 |
Any | 确定序列中是否有任何项目满足条件 |
Contains | 确定序列是否包含指定项目 |
限制 | |
Where | 筛选序列中的项目 |
设置 | |
Distinct | 返回无重复项目的序列 |
Except | 返回代表两个序列差集的序列 |
Intersect | 返回代表两个序列交集的序列 |
Union | 返回代表两个序列交集的序列 |
Lambda 表达式
许多标准查询操作符在对序列执行运算时都使用 Func 委托来处理单个元素。Lambda 表达式可与标准查询操作符结合使用以代表委托。lambda 表达式是创建委托实现的简略表达形式,并可用于匿名委托适用的所有场合。C# 和 Visual Basic
® .NET 均支持 Lambda 表达式。但是,必须注意:由于 Visual Basic .NET 尚不支持匿名方法,Lambda 表达式可能仅包含一个语句。
让我们来看看如何对一个整数数组使用 Single 操作符。这个整数数组的每个元素代表 2 的 1 到 10 次方。先创建此数组,然后使用 Single 操作符来检索满足 Lambda 表达式中指定条件的单个整数元素:
int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 }; int singleNum = nums.Single(x => x > 16 && x < 64); Console.WriteLine(singleNum.ToString());
Lambda 表达式包含多个关键部分。Lambda 表达式首先定义传入委托的变量。在以上代码示例中,x(在 => 操作符左侧声明)是参数,代表传递给它的 nums 数组中的每个元素。Lambda 表达式的剩余部分代表数组中每个元素的评估逻辑。可使用匿名委托轻松地重新编写以上表达式,如下所示:
但是,此代码的可读性不及 Lambda 表达式。C# 2.0 引入了可使委托的传递稍微轻松些的匿名委托;但是,Lambda 表达式的简洁语法可使其更加简单。
First 和 Single
如果必须从序列中提取一个值,First、FirstOrDefault、Single 和 SingleOrDefault 操作符都非常有用。First 方法返回序列中的第一个元素。First 有一个重载方法,可使用它来传入 Lambda 表达式来代表一个条件。例如,如果要返回整数序列中整数元素大于 50 的第一个元素,可使用以下代码示例:
此代码会查找第一个元素 (1)、大于 50 的第一个元素 (64) 以及大于 5,000 的第一个元素。由于数组中没有元素满足第三个 Lambda 表达式(数组中无整数大于 5,000),则如果代码使用的是 First 操作符而非 FirstOrDefault,则会引发异常。在使用 FirstOrDefault 操作符时,如果没有元素满足 Lambda 表达式,则会返回 0。First 操作符也可用于 LINQ to Entities 查询,如下所示:
在此示例中,将返回 London 城中的第一个客户。正如您所看到的,当 First 方法用于各种 LINQ 提供程序(在本例中为 LINQ to Objects 和 LINQ to Entities)时,所用的语法并不会更改。
在 LINQ to Entities 上下文中,First 操作符非常有用,尤其是您知道会从查询返回单个记录时。例如,您可能有个查询,它常在给出 CustomerID 时获取一条客户记录。这种情况总是返回 0 或 1 条记录,因此,得到一个序列不如就得到一个实体本身。换句话说,您宁愿获取 Customer 实体而非 1 个 Customer 实体序列。First 方法在某种怦下非常有用,如以下代码段所示。(由于实体框架不会尝试在客户端和服务器之间分发单个查询的执行,并且 LINQ to Entities 不支持 Single 方法,因此使用 First 方法是个轻松的替代方法。)
聚合、层次结构和投影
在 LINQ to Entities 查询中使用聚合操作符(如 Sum)可有助于简化查询。例如,以下代码检索订单总额大于 $10,000 的一个订单序列:
由于 LINQ 可查询层次结构实体集合,因此标准查询操作符也可用于对嵌套实体序列执行运算。当必须计算或询问派生数据时,这一点非常有用。派生数据可能仅存在于其基本窗体中,如客户订单的详细信息仅包含单价和数量值。在本例中,未在模型中的任何位置提供代表订单总金额的聚合数据。然而,通过在 LINQ 查询中应用 Sum 操作符,仍可检索消费金额超过 $20,000 的所有客户,如下所示:
此示例展示了如何在 LINQ 查询的多个层次应用标准查询操作符。查询最终会返回一个 Customers 实体序列,但为达到此目的,它必须首先深入每个客户的订单以及每个订单的订单详细信息获取所需数据,这样才可以计算每项的价格,汇总每个订单的项目,然后汇总每个客户的总额。
Count 操作符是另一聚合标准查询操作符。可通过使用以下代码确定有多少客户的消费金额超过 $25,000:
可使用 Max 操作符来确定最佳客户。以下代码示例将返回消费最高的客户所花费的金额。它在层次结构的多个层级中组合使用 Sum 和 Max 聚合操作符:
using (Entities entities = new Entities()) { var query = (from c in entities.Customers select new { c.CustomerID, Total = c.Orders.Sum( o => o.OrderDetails.Sum(od => od.UnitPrice)) }).Max(c2 => c2.Total); Console.WriteLine(query); }
投影和排序
您可能还注意到我在之前的示例中暗藏了一个投影。在使用 Max 操作符之前,LINQ 查询并不返回客户列表。而是会返回一个投影,此投影创建了包含 CustomerID 属性和 Total 属性(客户的整个消费金额)的一个新实体。投影是 LINQ 必不可少的一部分,如前一示例所示,将它们投影到序列中后,就可使用标准查询操作符来进一步处理它们。
图 1 显示了如何创建一个新实体投影,其中包含 CustomerID 和客户的订单总金额(使用之前讨论的 Sum 操作符)。
图 1 还使用 OrderByDescending 操作符来按计算总额对投影实体序列进行排序。如果两个客户总额相同,还会使用另一排序操作符来进一步定义顺序。例如,还可使用以下代码修正
图 1 中的 foreach 语句以进一步限定排序规则:
Figure 1 Aggregates, Projections, and Ordering
using (Entities entities = new Entities()) { var query = from c in entities.Customers where c.Orders.Sum( o => o.OrderDetails.Sum(od => od.UnitPrice)) > 0 select new { c.CustomerID, Total = c.Orders.Sum( o => o.OrderDetails.Sum(od => od.UnitPrice)) }; foreach (var item in query.OrderByDescending(x => x.Total)) Console.WriteLine(item.CustomerID + " == " + item.Total); }
foreach (var item in query.OrderByDescending(x => x.Total) .ThenBy(x => x.CustomerID)) { Console.WriteLine(item.CustomerID + " == " + item.Total); }
在该代码段中,我添加了 ThenBy 操作符和一个 Lambda 表达式,以表示序列应首先按 Total 属性降序排列,然后按投影的 CustomerID 属性升序排列。
限定符和转换
如果需要确定序列中是否存在某个值,可使用标准查询操作符 Any。限定符(如 Any、All 和 Contains)会搜索元素序列,并评估序列是否满足 lambda 表达式的条件。如果需检查序列以确定某些事宜(例如:是否存在来自特定地址的客户、所有客户是否来自同一国家或者任意其他分析确定性问题),它将非常有用。
例如,以下 LINQ 查询会检查是否来自 United Kingdom 的所有客户都位于 London。它使用限定符 All 并将其传递给仅评估城市是否为 London 的 lambda 表达式。如果序列中的每个元素都满足此条件并且 lambda 表达式返回 true,然后 All 操作符会返回 true:
using (Entities entities = new Entities()) { bool allUKCustomerAreFromLondon = (from c in entities.Customers where c.Country == "UK" select c).All( c => c.City.Equals("London")); Console.WriteLine(allUKCustomerAreFromLondon ? "Yes" : "No"); }
需在此查询中询问的另一问题是序列中是否有来自 United Kingdom 的 Cowes 的实体。对于此问题,可使用 Any 限定符来计算序列,如下所示:
using (Entities entities = new Entities()) { bool isOneUKCustomerFromCowes = (from c in entities.Customers where c.Country == "UK" select c).Any( c => c.City.Equals("Cowes")); Console.WriteLine(isOneUKCustomerFromCowes? "Yes" : "No"); }
Contains 操作符在评估序列中是否包括您所查找的项目时类似于 Any 操作符。Any 操作符可确定序列的某个项中是否存在某个值,而 Contains 操作符则确定序列中是否存在特定项目实例。例如,在将某个对象添加到序列中之前,您可能希望确保序列中并未包含该对象。
图 2 展示了如何检查。
Figure 2 Using Contains and Conversion
using (Entities entities = new Entities()) { Customers customerBSBEV = (from c in entities.Customers where c.CustomerID == "BSBEV" select c).First(); var customersUK = from c in entities.Customers where c.Country == "UK" select c; bool isCustomerInSequence = customersUK.Contains(customerBSBEV); Console.WriteLine(isCustomerInSequence? "Yes" : "No"); }
请注意:在
图 2 中,首先针对 BSBEV 客户检索 Customers 实体。然后,检索客户来自 United Kingdom 的 Customers 实体序列。最后,使用 Contains 操作符来检查 Customers 序列是否包含 customerBSBEV 变量的实例。
图 2 中所显示的 Contains 操作符实现适用于可基于其实际实例信心十足地比较对象的场合。但是,如果需要 Contains 操作符根据逻辑标识进行测试又该如何呢?幸运的是,Contains 操作符包含一个重载,可使用它来传递实现 IEqualityComparer<T> 接口的对象。要根据 CustomerID 使用 Contains,可按如下所示重新编写
图 2 中的代码:
using (Entities entities = new Entities()) { ... bool isCustomerInSequence = customersUK.Contains(customerBSBEV, new CustomerComparer()); Console.WriteLine(isCustomerInSequence? "Yes" : "No"); }
其中 CustomerComparer 定义为
结束语
有许多标准查询操作符均可定义为 Enumerable 和 Queryable 序列类的扩展方法。如我之前所示,这些操作符有助于扩展 LINQ 的功能。我还展示了结合使用多个 .NET Framework 3.5 新增强功能(包括 lambda 表达式、LINQ、实体框架和隐式类型化变量)来更加轻松地编写功能强大的代码和逻辑。
请将您想向 John 询问的问题和提出的意见发送至 mmdatat@microsoft.com.
John Papa 是 ASPSOFT (
aspsoft.com) 的一位资深 .NET 顾问,同时也是一位狂热的棒球迷,在夏季的大多数夜晚,他都与家人以及忠实的狗 Kadi 一起为洋基队加油。John 是 C# 领域的一位 MVP 和 INETA 发言人,撰写过多本有关数据访问技术方面的书籍。他经常在行业会议上发表演讲,或者在
http://www.johnpapa.net/ 上撰写博客文章。