第9章 LINQ运算符
l 本章将逐一描述LINQ查询运算符。作为参考,“9.3 投影”和“9.4 联接”两节会提及几个概念领域:
l • 投影对象层次
l • 用Select、SelectMany、Join和GroupJoin进行联接
l • 查询内涵式语法中的外部迭代变量
l 本章的所有示例都采用如下定义的names数组:
l
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
l 使用LINQ to SQL技术的示例采用一个类型化为DataContext的变量,名为dataContext:
l
var dataContext = new DemoDataContext( );
...
public class DemoDataContext : DataContext
{
public DemoDataContext (string cxString) : base (cxString) {}
public Table<Customer> Customers { get { return GetTable<Customer>( ); } }
public Table<Purchase> Purchases { get { return GetTable<Purchase>( ); } }
}
[Table] public class Customer
{
[Column(IsPrimaryKey=true)] public int ID;
[Column] public string Name;
[Association (OtherKey="CustomerID")]
public EntitySet<Purchase> Purchases = new EntitySet<Purchase>( );
}
[Table] public class Purchase
{
[Column(IsPrimaryKey=true)] public int ID;
[Column] public int? CustomerID;
[Column] public string Description;
[Column] public decimal Price;
[Column] public DateTime Date;
EntityRef<Customer> custRef;
[Association (Storage="custRef",ThisKey="CustomerID",IsForeignKey=true)]
public Customer Customer
{
get { return custRef.Entity; } set { custRef.Entity = value; }
}
}
l 代码中所显示的LINQ to SQL实体类,是自动化的工具所生成的类的简化版,少了部分代码。这部分代码关联双方的实体被重新指定时,更新关联的另一方。
l
l 这是以上实体类相对应的SQL表定义:
l
create table Customer
(
ID int not null primary key,
Name varchar(30) not null
)
create table Purchase
(
ID int not null primary key,
CustomerID int references Customer (ID),
Description varchar(30) not null,
Price decimal not null
)
9.1 概述
l 本节,我们概括性地描述C# 3.0所支持的查询运算符。C#查询运算符一般都属于三类中的一种,这是这三种分类的简单表示:
l • 输入集合,输出集合(集合到集合)
l • 输入集合,输出单个元素或标量值
l • 输入单个元素或标量值,输出集合
l 我们先列出这三大类别中的每一类及其包含的查询运算符,然后我们再详细介绍各个查询运算符。
9.1.1 集合à集合
l 大多数查询运算符接收一个或多个输入集合,得到一个或多个输出集合。图9-1阐明了重组集合形状的查询运算符。
l
图9-1 改变形状的运算符
1. 筛选
IEnumerable<TSource> à IEnumerable<TSource>
l 返回原始集合的子集:
l
Where, Take, TakeWhile, Skip, SkipWhile, Distinct
2. 投影
IEnumerable<TSource> à IEnumerable<TResult>
l 依据lambda函数对每个元素进行转换。SelectMany展平嵌套的集合;Select 和SelectMany利用LINQ to SQL技术实现内部联接、左外部联接、交叉联接以及不等联接。
l
Select, SelectMany
3. 联接
IEnumerable<TOuter>, IEnumerable<TInner> à IEnumerable<TResult>
l 将一个集合中的元素与另一个集合中的元素关联起来。联接运算符在本地查询中执行高效,它支持内部联接和左外部联接。
l
Join, GroupJoin
4. 排序
IEnumerable<TSource> à IOrderedEnumerable<TSource>
l 返回重新排序后的集合。
l
OrderBy, ThenBy, Reverse
5. 分组
IEnumerable<TSource> à IEnumerable<IGrouping<TSource,TElement>>
l 将一个集合分成多个子集。
l
GroupBy
6. Set 操作
IEnumerable<TSource>, IEnumerable<TSource> à IEnumerable<TSource>
l 取两个类型相同的集合,返回它们的交集、并集或差集。
l
Concat, Union, Intersect, Except
7. 转换方法:Import
IEnumerable à IEnumerable<TResult>
OfType, Cast
8. 转换方法:Export
IEnumerable<TSource> à An array, list, dictionary, lookup, or sequence
IEnumerable<TSource> à 数组,列表,字典,一对多字典(lookup)或序列
ToArray, ToList, ToDictionary, ToLookup, AsEnumerable, AsQueryable
9.1.2 集合à非集合
l 以下查询运算符接收一个输入序列,输出单个元素或标量值。
1. 元素操作
IEnumerable<TSource> à TSource
l 从集合众选出一个元素。
l
First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault,
ElementAt, ElementAtOrDefault, DefaultIfEmpty
2. 聚合方法
IEnumerable<TSource> à scalar
l 对集合进行计算,返回一个标量值(有代表性的为一个数字)。
l
Aggregate, Average, Count, LongCount, Sum, Max, Min
3. 限定符
IEnumerable<TSource> à bool
l 一种返回true或false的聚合。
l
All, Any, Contains, SequenceEqual
9.1.3 非集合à集合
l 第三种也就是最后一种运算符,不接收任何输入集合,从无到有生成一个输出集合。
生成方法
void à IEnumerable<TResult>
l 生成一个简单的序列。
Empty, Range, Repeat
9.2 筛选
IEnumerable<TSource> à IEnumerable<TSource>
方法 |
描述 |
等价的SQL方法 |
Where |
返回满足给定条件的元素子集 |
WHERE |
Take |
返回前count个元素,舍弃其余元素 |
WHERE ROW_NUMBER( )... 或TOP n子查询 |
Skip |
忽略前count个元素,返回其余元素 |
WHERE ROW_NUMBER( )... 或NOT IN (SELECT TOP n...) |
TakeWhile |
直到谓词为true时,才输出输入序列中的元素 |
抛出异常 |
SkipWhile |
忽略为此为true时输入序列中的元素,输出其余元素 |
抛出异常 |
Distinct |
返回一个不含重复元素的集合 |
SELECT DISTINCT... |
l 在本章中,参考表中的“等价的SQL方法”一列没必要与IQueryable所实现的相对应,如与LINQ to SQL产生的相对应。确切地说,它是用来说明,如果你要自己编写SQL查询,你应当用什么来处理同样的事情。如果没有简单的转换,则这一列留空。如果完全没有转换,则这一列为“抛出异常”。
l 显示时,Enumerable类的实现代码不包含空参数和索引谓词的检验。
l
l 使用任意一种筛选方法,你最终都会得到与起始序列数目相同或更少的元素。决不会得到更多的元素!并且得到的元素也是相同的;不会对它们进行任何转换。
9.2.1 Where
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
谓词 |
TSource => bool 或 (TSource,int) => bool |
1. 广义语法
where bool-expression
2. Enumerable.Where的实现
l 不考虑空类型检查,Enumerable.Where的内部实现从功能上来说与如下代码等价:
l
public static IEnumerable<TSource> Where<TSource>
(this IEnumerable<TSource> source, Func <TSource, bool> predicate)
{
foreach (TSource element in source)
if (predicate (element))
yield return element;
}
3. 概述
l Where返回输入序列中满足给定布尔条件的元素。
l 例如:
l
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query = names.Where (name => name.EndsWith ("y"));
// Result: { "Harry", "Mary", "Jay" }
l 用广义语法表示:
l
IEnumerable<string> query = from n in names
where n.EndsWith ("y")
select n;
l 一个查询中,where语句可以出现多次,中间要穿插let语句:
l
from n in names
where n.Length > 3
let u = n.ToUpper( )
where u.EndsWith ("Y")
select u; // Result: { "HARRY", "MARY" }
l 标准的C#范围规则对这样的查询有效。换句话说,你不能在用迭代变量或let语句声明变量之前引用该变量。
4. 索引筛选
l Where的布尔条件还可以接收第二个参数,为int类型。该参数用来提供输入序列中元素的位置信息,布尔条件可以将此信息用于其筛选决策中。例如,下面的例子每逢第二个元素就跳过:
l
IEnumerable<string> query = names.Where ((n, i) => i % 2 == 0);
// Result: { "Tom", "Harry", "Jay" }
l 如果在LINQ to SQL中使用索引筛选则会抛出异常。
5. LINQ to SQL中的Where
l 如下字符串方法转换成SQL的LIKE运算符:
l
Contains, StartsWith, EndsWith
l 例如,c.Name.Contains ("abc")转换成customer.Name LIKE '%abc%'(或更为精确地,带参数的语句)。调用SqlMethods.Like,你可以进行更复杂的比较。该方法直接映射到SQL的LIKE运算符。利用字符串的CompareTo方法,你还进行字符串的顺序比较;该方法映射到SQL's < 和 >运算符:
l
dataContext.Purchases.Where (p => p.Description.CompareTo ("C") < 0)
l LINQ to SQL还支持在筛选的谓词中将Contains运算符用于本地集合。例如:
l
string[] chosenOnes = { "Tom", "Jay" };
from c in dataContext.Customers
where chosenOnes.Contains (c.Name)
...
l 这段代码映射到SQL的IN运算符则表示如下:
l
WHERE customer.Name IN ("Tom", "Jay")
l 如果本地集合为实体数组或不是标量类型,LINQ to SQL则可能会输出一条EXISTS语句。
9.2.2 Take和Skip
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
选取或跳过得元素个数 |
int |
l Take输出前n个元素,舍弃其余元素;Skip舍弃前n个元素,输出其余元素。当要实现能支持用户浏览大量匹配记录的网页时,这两种方法都很有用。例如,假设用户搜索一个书目数据库中的“mercury”项,有100条匹配数据。如下代码返回前20条数据:
l
IQueryable<Book> query = dataContext.Books
.Where (b => b.Title.Contains ("mercury"))
.OrderBy (b => b.Title)
.Take (20);
l 下一个查询返回第21至第40条数据:
l
IQueryable<Book> query = dataContext.Books
.Where (b => b.Title.Contains ("mercury"))
.OrderBy (b => b.Title)
.Skip (20).Take (20);
l LINQ to SQL将Take和Skip转换成SQL Server 2005中的ROW_NUMBER函数,或是更早版本SQL Server中的TOP n子查询。
9.2.3 TakeWhile和SkipWhile
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
谓词 |
bool 或(TSource,int) => bool |
l TakeWhile枚举输入序列,输出各项元素,直到给定的布尔条件为true。它忽略剩余的元素:
l
int[] numbers = { 3, 5, 2, 234, 4, 1 };
var takeWhileSmall = numbers.TakeWhile (n => n < 100); // { 3, 5, 2 }
l SkipWhile枚举输入序列,忽略各项元素,直到给定的布尔条件为true。它输出剩余的元素:
l
int[] numbers = { 3, 5, 2, 234, 4, 1 };
var skipWhileSmall = numbers.SkipWhile (n => n < 100); // { 234, 4, 1 }
l TakeWhile和SkipWhile在SQL中没有对应的转换,如果用在LINQ to SQL查询中,会导致运行时错误。
9.2.4 Distinct
l Distinct返回去掉重复后的输入序列。只有默认的相等比较器可以用于相等比较。如下代码返回字符串中的不同字母:
l
char[] distinctLetters = "HelloWorld".Distinct().ToArray( );
string s = new string (distinctLetters); // HeloWrd
l 字符串可以直接调用LINQ方法,因为字符串实现IEnumerable<char>接口。
9.3 投影
IEnumerable<TSource> à IEnumerable<TResult>
方法 |
描述 |
等价的SQL方法 |
Select |
根据给定的lambda表达式对每个输入元素进行转换 |
SELECT |
SelectMany |
对每个输入元素进行转换,然后展平并连接结果序列 |
INNER JOIN,LEFT OUTER JOIN,CROSS JOIN |
l 对于LINQ to SQL查询而言,Select和SelectMany是最通用的联接结构;对本地查询而言,则Join和GroupJoin是最有效的联接结构。
l
9.3.1 Select
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
结果选择器 |
TSource => TResult或(TSource,int) => TResult |
1. 广义语法
select projection-expression
2. Enumerable类的实现
public static IEnumerable<TResult> Select<TSource,TResult>
(this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
foreach (TSource element in source)
yield return selector (element);
}
3. 概述
l 运用Select运算符,你得到的元素个数总是与起始时相同。每个元素都可以通过lambda函数进行任意形式的转换。
l 如下代码选取所有安装在计算机上的字体名称(从System.Drawing中):
l
IEnumerable<string> query = from f in FontFamily.Families
select f.Name;
foreach (string name in query) Console.WriteLine (name);
l 这个例子中,select语句将FontFamily对象转化为其名称。这是等价的lambda表示:
l
IEnumerable<string> query = FontFamily.Families.Select (f => f.Name);
l Select语句常用于投影到匿名类型:
l
var query =
from f in FontFamily.Families
select new { f.Name, LineSpacing = f.GetLineSpacing (FontStyle.Bold) };
l 不进行任何转换的投影有时会用在广义查询中,从而满足以select或group语句结束的查询需求。如下代码选取支持strikeout的字体:
l
IEnumerable<FontFamily> query =
from f in FontFamily.Families
where f.IsStyleAvailable (FontStyle.Strikeout)
select f;
foreach (FontFamily ff in query) Console.WriteLine (ff.Name);
l 对于这样的情形,编译器在将其转化成lambda语法时会略去投影。
4. 索引投影
l 选择器表达式还可以接收一个整数参数,作为索引给表达式提供输入序列中每个元素得位置信息。该参数只在本地查询中有效:
l
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query = names
.Select ((s,i) => i + "=" + s); // { "0=Tom", "1=Dick", ... }
5. 子查询和对象层次
l 你可以在select语句中嵌套子查询,从而建立起对象层次。下面的例子返回一个描述D:\source目录下各个目录的集合,该集合包含有每个目录下文件的子集合:
l
DirectoryInfo[] dirs = new DirectoryInfo (@"d:\source").GetDirectories( );
var query =
from d in dirs
where (d.Attributes & FileAttributes.System) == 0
select new
{
DirectoryName = d.FullName,
Created = d.CreationTime,
Files = from f in d.GetFiles( )
where (f.Attributes & FileAttributes.Hidden) == 0
select new { FileName = f.Name, f.Length, }
};
foreach (var dirFiles in query)
{
Console.WriteLine ("Directory: " + dirFiles.DirectoryName);
foreach (var file in dirFiles.Files)
Console.WriteLine (" " + file.FileName + "Len: " + file.Length);
}
l 该查询的内部可以成为相关联的子查询。如果子查询引用了外部查询的对象,则该子查询就是相关联的。在这个例子中,子查询引用了d,即被枚举的目录。
l
l Select内部的子查询使得你可以将一个对象层次映射到另一个对象层次,或是将一个关系对象模型映射到一个层次对象模型。
l
l 对于本地查询,Select内的子查询会导致双重延迟执行。在我们的例子中,直到内层foreach语句枚举时,才会筛选或投影文件。
6. LINQ to SQL中的子查询和联接
l 子查询投影在LINQ to SQL中表现良好,可以用来完成SQL样式的联接工作。此处给出如何检索每个客户姓名及其高价值采购的代码:
l
var query =
from c in dataContext.Customers
select new {
c.Name,
Purchases = from p in dataContext.Purchases
where p.CustomerID == c.ID && p.Price > 1000
select new { p.Description, p.Price }
};
foreach (var namePurchases in query)
{
Console.WriteLine ("Customer: " + namePurchases.Name);
foreach (var purchaseDetail in namePurchases.Purchases)
Console.WriteLine (" - $$$: " + purchaseDetail.Price);
}
l 这种查询样式非常适用于解释查询。LINQ to SQL将外部查询和子查询作为一个单元来处理,以避免不必要的往返通信。对于本地查询而言,这种查询样式效率较低,因为每一个内部和外部元素的组合都必须经过枚举以获得为数不多的配对组合。对本地查询来说,Join 或GroupJoin会是更好的选择,我们会在后面的小节中介绍。
l
l 该查询将两个独立集合中的对象匹配起来,可以把它看作一种“联接”。它与传统的数据库联接(或子查询)的区别在于,我们不是将输出展平成一个二维的结果集。我们是将相关的数据映射到层次数据上,而不是平面数据上。
l 利用Customer实体上的Purchases关联属性,可以得到简化后的同样的查询:
l
from c in dataContext.Customers
select new
{
c.Name,
Purchases = from p in c.Purchases // Purchases is EntitySet<Purchase>
where p.Price > 1000
select new { p.Description, p.Price }
};
l 在外部枚举时获得全部客户,不考虑他们有无采购。在这样的情景下,上述的两种查询都类似于SQL中的左外部查询。仿效内部联接,即不包含采购价值低的客户,我们需要在采购集合上添加一个筛选条件:
l
from c in dataContext.Customers
where c.Purchases.Any (p => p.Price > 1000)
select new {
c.Name,
Purchases = from p in c.Purchases
where p.Price > 1000
select new { p.Description, p.Price }
};
l 这段代码不够简练,因为我们写了同一个谓词(Price > 1000)两次。我们可以利用let语句来避免这一重复:
l
from c in dataContext.Customers
let highValueP = from p in c.Purchases
where p.Price > 1000
select new { p.Description, p.Price }
where highValueP.Any( )
select new { c.Name, Purchases = highValueP };
l 这样的查询样式则灵活多了。例如,把Any改为Count,我们可以将查询修改为只获取至少有两项高价值采购的客户:
l
...
where highValueP.Count( ) >= 2
select new { c.Name, Purchases = highValueP };
7. 投影为具体类型
l 投影为匿名类型在获取中间结果方面很有用,但是如果你想要将结果集发回客户端,则投影为匿名类型就没什么用处了。因为匿名类型只能在某个方法内部作为局部变量存在。替代的方法就是,将具体类型用于投影,例如DataSets或定制的业务实体类。定制的业务实体类其实就是你自己编写的一个类,具有某些属性,与LINQ to SQL的带[Table]注解的类相似,不过隐藏了低层次(数据库相关)的细节。例如,你可以不把外键包含在业务实体类中。假设我们编写了定制的业务实体类,分别名为CustomerEntity和PurchaseEntity。如下代码显示如何对其进行投影:
l
IQueryable<CustomerEntity> query =
from c in dataContext.Customers
select new CustomerEntity
{
Name = c.Name,
Purchases =
(from p in c.Purchases
where p.Price > 1000
select new PurchaseEntity {
Description = p.Description,
Value = p.Price
}
).ToList( )
};
// Force query execution, converting output to a more convenient List:
List<CustomerEntity> result = query.ToList( );
l 注意,到目前为止,我们都没有使用Join或SelectMany语句。这是因为我们保留了数据的层次形状,如图9-2所示。利用LINQ,无需使用传统的SQL方法,就可以将表展平为二维的结果集。
l
图9-2 投影对象层次
9.3.2 SelectMany
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
结果选择器 |
TSource => IEnumerable<TResult>或(TSource,int) => IEnumerable<TResult> |
1.广义语法
from identifier
from identifier
...
2.Enumerable类的实现
public static IEnumerable<TResult> SelectMany<TSource,TResult>
(IEnumerable<TSource> source,
Func <TSource,IEnumerable<TResult>> selector)
{
foreach (TSource element in source)
foreach (TResult subElement in selector (element))
yield return subElement;
}
3.概述
l SelectMany将子序列连接成为一个扁平的输出序列。
l 回想一下,对于每一个输入元素,Select只生成一个输出元素。相反,SelectMany生成0..n个输出元素。这0..n个元素来自于lambda表达式生成的子序列或孩子序列。
l SelectMany可以用于扩展孩子序列,展平嵌套的集合,并将两个集合联接为一个扁平的输出序列。沿用传送带这一比喻,SelectMany选取新的材料送上传送带。运用SelectMany,每一个输入元素都能触发新材料的引入。新材料由选择器lambda表达式输出,它必须是一个序列。换句话说,lambda表达式必须对每一个输入元素输出一个孩子序列。最终结果是针对每一个输入元素输出一连串孩子序列。举个简单的例子,假设我们有一个名字数组如下:
l
string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" };
l 我们希望将其转换为一个单词的扁平集合,也就是:
l
"Anne", "Williams", "John", "Fred", "Smith", "Sue", Green"
l SelectMany能很好地完成这项任务,因为可以将一个输入元素映射到多个输出元素。我们所需做的只是提出一个选择器表达式,将每个输入元素转换为一个孩子序列。string.Split完美地完成这一任务:取一个字符串,将它分割为每个字,以一个数组的形式输出结果:
l
string testInputElement = "Anne Williams";
string[] childSequence = testInputElement.Split( );
// childSequence is { "Anne", "Williams" };
l 这是我们的SelectMany查询和所得结果:
l
IEnumerable<string> query = fullNames.SelectMany (name => name.Split( ));
foreach (string name in query)
Console.Write (name + "|"); // Anne|Williams|John|Fred|Smith|Sue|Green|
l 如果用Select替代SelectMany,在层次形式上会获得同样的结果。如下代码输出一个字符串数组序列,需要使用嵌套的foreach语句来枚举:
l
IEnumerable<string[]> query =
fullNames.Select (name => name.Split( ));
foreach (string[] stringArray in query)
foreach (string name in stringArray)
Console.Write (name + "/");
l SelectMany的好处就是它生成一个扁平的结果序列。
l
l 查询广义语法支持SelectMany运算符,并且SelectMany可以被附加生成器(也就是查询中的额外的from语句)所调用。From关键字在广义语法中有两种含义。在查询的开头,它引入初始迭代变量和输入序列。在查询的其他位置,它转换为SelectMany。我们的查询用广义语法表达如下:
l
IEnumerable<string> query =
from fullName in fullNames
from name in fullName.Split( ) // Translates to SelectMany
select name;
l 注意,附加的生成器引入一个新的查询变量,这个例子中为name。从那会开始,新的查询变量就成为迭代变量,而之前的迭代变量则降级为一个外部的迭代变量。
4.外部迭代变量
l 在前面那个例子中,fullName在SelectMany之后成为外部迭代变量。外部变量始终在作用范围内,直到查询终止或到达一条into语句。对广义语法而言,这些变量的扩展范围即为其优于lambda语法的杀手锏。
l 我们可以拿前面那个查询来举例说明,将fullName包含在最终的投影中:
l
IEnumerable<string> query =
from fullName in fullNames // fullName = outer variable
from name in fullName.Split( ) // name = iteration variable
select name + " came from " + fullName;
Anne came from Anne Williams
Williams came from Anne Williams
John came from John Fred Smith
...
l 编译器在后台必须用些技巧来解决外部引用问题。以lambda语法来编写同样的查询是个不错的解决方法。这是需要技巧的!如果你在投影之前插入一条where或orderby语句,则会变得更难:
l
from fullName in fullNames
from name in fullName.Split( )
orderby fullName, name
select name + " came from " + fullName;
l 问题是,SelectMany输出孩子元素的扁平序列,在我们的例子中,即为单词的扁平序列。孩子元素所来源的外部元素(fullName)丢失了。解决方案是,在一个临时的匿名类型中,包含孩子元素及其外部元素:
l
from fullName in fullNames
from x in fullName.Split( ).Select (name => new { name, fullName } )
orderby x.fullName, x.name
select x.name + " came from " + x.fullName;
l 此处唯一的改变就是,我们将每个孩子元素(name)封装在一个匿名类型中,该匿名类型还包含其fullName。这与let语句的解析方法相似。以下为转换为lambda语法的最终形式:
l
IEnumerable<string> query = fullNames
.SelectMany (fName => fName.Split( )
.Select (name => new { name, fName } ))
.OrderBy (x => x.fName)
.ThenBy (x => x.name)
.Select (x => x.name + " came from " + x.fName);
5.广义语法编程思想
l 正如我们刚刚展示的,如果你需要外部迭代变量,则有充分的理由使用广义语法。在这样的情况下,不仅有助于使用广义语法,而且还有助于按照广义语法的思想思考。
l 编写附加生成器时有两种基本模式。第一种是,扩展并展平子序列。为实现这一点,要在你的附加生成器中,调用一个已有查询变量的属性或方法。我们在之前的例子中已经这么做了:
l
from fullName in fullNames
from name in fullName.Split( )
l 此处,我们已经从枚举完整的名字扩展到枚举单词。在LINQ to SQL中,当你扩展孩子关联属性时,有相似的查询。如下查询列出所有客户及其采购:
l
IEnumerable<string> query = from c in dataContext.Customers
from p in c.Purchases
select c.Name + " bought a " + p.Description;
Tom bought a Bike
Tom bought a Holiday
Dick bought a Phone
Harry bought a Car
...
l 此处,我们把每一个客户都扩展到采购子序列中。
l 第二种模式是计算交叉乘积或执行交叉联接,序列中的每个元素与另一个序列中的各个元素一一匹配。引入一个生成器来实现,该生成器的选择器表达式返回一个与迭代变量无关的序列:
l
int[] numbers = { 1, 2, 3 }; string[] letters = { "a", "b" };
IEnumerable<string> query = from n in numbers
from l in letters
select n.ToString( ) + l;
RESULT: { "
l 这种查询样式是SelectMany样式联接的基础。
6. 利用SelectMany联接
l 你可以用SelectMany来联接两个序列,仅通过筛选交叉乘积的结果。例如,假设我们想要搭配竞赛选手。我们可以这样开始:
l
string[] players = { "Tom", "Jay", "Mary" };
IEnumerable<string> query = from name
from name
select name1 + " vs " + name2;
RESULT: { "Tom vs Tom", "Tom vs Jay", "Tom vs Mary",
"Jay vs Tom", "Jay vs Jay", "Jay vs Mary",
"Mary vs Tom", "Mary vs "Jay", "Mary vs Mary" }
l 查询可直接理解为:“对每个选手,重复每个选手,选择选手1对选手
l
IEnumerable<string> query = from name
from name
where name1.CompareTo (name2) < 0
orderby name1, name2
select name1 + " vs " + name2;
RESULT: { "Jay vs Mary", "Jay vs Tom", "Mary vs Tom" }
l 筛选器的谓词构成联接条件。我们的查询可以称作是不等联接,因为联接条件没有使用相等运算符。
l 我们将利用LINQ to SQL演示联接的其余类型。
7. LINQ to SQL中的SelectMany
l LINQ to SQL中的SelectMany可以执行交叉联接、不等联接、内部联接和左外部联接。你可以与预定义的关联和特设的关系一起使用SelectMany,这与Select一样。区别在于,SelectMany返回一个扁平结果集,而非层次结果集。
l LINQ to SQL中的交叉联接编写方式与前面的小节一样。如下查询将每一位客户与每一项采购相匹配(一种交叉联接):
l
var query = from c in dataContext.Customers
from p in dataContext.Purchases
select c.Name + " might have bought a " + p.Description;
l 更典型一点的,你可能只想将客户与其自己的采购相匹配。添加一条带有联接谓词的where语句就可以实现。这样会得到一个标准的SQL风格的同等联接:
l
var query = from c in dataContext.Customers
from p in dataContext.Purchases
where c.ID == p.CustomerID
select c.Name + " bought a " + p.Description;
l 这段代码能很好地转化为SQL语句。在下一节中,我们会看到它如何扩展到能够支持外部联接。事实上,再用LINQ的Join运算符表示这些查询会使其扩展性更差——在这一点上,LINQ与SQL是相反的。
l
l 如果有LINQ to SQL实体中关系的关联属性,你可以通过扩展子集合来表示同一个查询,而不用通过筛选交叉乘积的结果:
l
from c in dataContext.Customers
from p in c.Purchases
select new { c.Name, p.Description };
l 其优势在于,去除了联接谓词。我们已经经历了从交叉乘积到扩张和展平。不过,这两种查询都会得到同样的SQL语句。
l 你可以给这类查询添加where语句用于附加筛选。例如,如果我们只想要名字以“T”开头的客户,我们可以按如下方式进行筛选:
l
from c in dataContext.Customers
where c.Name.StartsWith ("T")
from p in c.Purchases
select new { c.Name, p.Description };
l 如果where语句下移一行,该LINQ to SQL查询就能执行得相当好。如果它是本地查询,那么将where语句下移会使其效率降低。对待本地查询,你应当在联接之前进行筛选。
l 你还可以再附加from语句来引入新表。例如,如果每一项采购都有采购项子数据,你就可以生成一个客户的扁平结果集,每一位客户都附有其采购信息,而每一项采购又附有其采购项的详细信息,实现代码如下:
l
from c in dataContext.Customers
from p in c.Purchases
from pi in p.PurchaseItems
select new { c.Name, p.Description, pi.DetailLine };
l 每一条from语句都引入一个新的子表。要包含父表中的数据(通过关联属性),无需添加from语句,只要定位到该属性就行。例如,如果每个客户都对应有一个销售员,想要查询销售员姓名,只需做如下事情:
l
from c in dataContext.Customers
select new { Name = c.Name, SalesPerson = c.SalesPerson.Name };
l 在这个例子中不用SelectMany是因为没有子集需要展平。父关联属性返回一个单项。
8. 利用SelectMany实现外部联接
l 我们之前所看到的是,Select子查询生成类似于作外部查询的结果。
l
from c in dataContext.Customers
select new {
c.Name,
Purchases = from p in c.Purchases
where p.Price > 1000
select new { p.Description, p.Price }
};
l 这个例子包含了每一个外部元素(customer),不考虑客户有无采购。不过假设我们用SelectMany重写这一查询,我们则会得到一个扁平集合,而非层次结果集合:
l
from c in dataContext.Customers
from p in c.Purchases
where p.Price > 1000
select new { c.Name, p.Description, p.Price };
l 在展平查询的过程中,我们已经将其转换为一个内部查询了:只包含哪些有一项或多项高额度采购的客户。要得到输出扁平结果集的左外部联接,我们必须将DefaultIfEmpty查询运算符应用于内部序列。如果输入序列没有元素,则该方法返回空。这是一个此类查询示例,其中未包含价格谓词:
l
from c in dataContext.Customers
from p in c.Purchases.DefaultIfEmpty( )
select new { c.Name, p.Description, Price = (decimal?) p.Price };
l 这段代码在LINQ to SQL中执行良好,返回所有客户,不管其有没有采购。但是如果我们要将其作为本地查询执行,就会失败。因为当p为空时,p.Description和p.Price会抛出异常。无论哪种情形,我们都可以按如下方式使查询更加稳健:
l
from c in dataContext.Customers
from p in c.Purchases.DefaultIfEmpty( )
select new {
c.Name,
Descript = p == null ? null : p.Description,
Price = p == null ? (decimal?) null : p.Price
};
l 现在我们再来引入价格筛选器。我们不能像以前那样使用where语句了,因为它会在DefaultIfEmpty之后执行。
l
from c in dataContext.Customers
from p in c.Purchases.DefaultIfEmpty( )
where p.Price > 1000...
l 正确的方法是,利用子查询将Where语句接在DefaultIfEmpty之前:
l
from c in dataContext.Customers
from p in c.Purchases.Where (p => p.Price > 1000).DefaultIfEmpty( )
select new {
c.Name,
Descript = p == null ? null : p.Description,
Price = p == null ? (decimal?) null : p.Price
};
l 这段代码在LINQ to SQL中会转化为左外部联接,并且对于编写这一类型的查询而言,此段代码是一种有效的模式。
l
l 如果你习惯了用SQL编写外部联接,对于这一类查询,你很可能想忽略Select子查询这一更简单的选择,倾向于笨拙但却是你所熟悉的、以SQL为主的扁平方法。来源于Select子查询的层次结果集通常更适合于外部联接类查询,因为无需处理额外的空变量。
l
9.4 联接
IEnumerable<TOuter>, IEnumerable<TInner> à IEnumerable<TResult>
方法 |
描述 |
等价的SQL方法 |
Join |
应用查找策略来匹配两个集合中的元素,输出扁平结果集 |
INNER JOIN |
GroupJoin |
同上,但输出层次结果集 |
INNER JOIN,LEFT OUTER JOIN |
9.4.1 Join和GroupJoin
1. Join的参数
参数 |
类型 |
外部序列 |
IEnumerable<TOuter> |
内部序列 |
IEnumerable<TInner> |
外部关键选择器 |
TOuter => TKey |
内部关键选择器 |
TInner => TKey |
结果选择器 |
(TOuter,TInner) => TResult |
2. GroupJoin参数
参数 |
类型 |
外部序列 |
IEnumerable<TOuter> |
内部序列 |
IEnumerable<TInner> |
外部关键选择器 |
TOuter => TKey |
内部关键选择器 |
TInner => TKey |
结果选择器 |
(TOuter,IEnumerable<TInner>) => TResult |
3. 广义语法
from outer-var in outer-enumerable
join inner-var in inner-enumerable on outer-key-expr equals inner-key-expr
[ into identifier ]
4. 概述
l Join和GroupJoin将两个输入序列合成为一个输出序列。Join输出扁平输出序列,GroupJoin输出层次输出序列。
l Join和GroupJoin可以替代Select和SelectMany。Join和GroupJoin的优势在于,其在本地内存中的集合上执行很有效,因为他们先将内部序列加载到关键字索引中,从而避免重复枚举各个内部元素。其劣势在于,它们只能实现等价的内部连接和左外部联接;交叉联接和不等联接仍然必须通过Select/SelectMany来实现。而在LINQ to SQL查询中,Join和GroupJoin相对Select和SelectMany而言没有什么真正的优势。
l 表9-1总结了各个连接策略的不同之处。
表9-1 联接策略
策略 |
结果形状 |
本地查询效率 |
内部联接 |
左外部联接 |
交叉联接 |
不等联接 |
Select + SelectMany |
扁平 |
差 |
可以 |
可以 |
可以 |
可以 |
Select + Select |
嵌套 |
差 |
可以 |
可以 |
可以 |
可以 |
Join |
扁平 |
好 |
可以 |
— |
— |
— |
GroupJoin |
嵌套 |
好 |
可以 |
可以 |
— |
— |
GroupJoin + SelectMany |
扁平 |
好 |
可以 |
可以 |
— |
— |
5. Join
l Join运算符执行内部联接,得到扁平状输出序列。
l 演示Join的最简单的方式就是利用LINQ to SQL。以下查询列出全部客户及其采购,但没有使用关联属性:
l
IQueryable<string> query =
from c in dataContext.Customers
join p in dataContext.Purchases on c.ID equals p.CustomerID
select c.Name + " bought a " + p.Description;
l 该查询的结果与我们由SelectMany样式的查询所得的结果相当:
l
Tom bought a Bike
Tom bought a Holiday
Dick bought a Phone
Harry bought a Car
l 要看到Join优于SelectMany之处,必须将此查询转换为本地查询。先将所有的客户及其采购数据复制到一个数组中,然后对这一数组进行查询就可以实现了:
l
Customer[] customers = dataContext.Customers.ToArray( );
Purchase[] purchases = dataContext.Purchases.ToArray( );
var slowQuery = from c in customers
from p in purchases where c.ID == p.CustomerID
select c.Name + " bought a " + p.Description;
var fastQuery = from c in customers
join p in purchases on c.ID equals p.CustomerID
select c.Name + " bought a " + p.Description;
l 尽管这两种查询获得同样的结果,但是Join查询要快得多,因为Enumerable类中对Join的实现将内部集合(purchases)预载到了关键字索引中了。
l Join的广义语法大体表示如下:
l
join inner-var in inner-sequence on outer-key-expr equals inner-key-expr
l LINQ中的join运算符区分外部序列和内部序列。从语句构成上来说,具体如下:
外部序列
l 输入序列(在本例中即为customers)。
l
内部序列
l 引入的新的集合(在本例中为purchases)。
l
l Join执行内部联接,因而无采购的客户不会出现在输出序列中。对于内部联接,你可以交换内部和外部序列的位置,仍然会得到同样的结果:
l
from p in purchases // p is now outer
join c in customers on p.CustomerID equals c.ID // c is now inner
...
l 你可以给这条查询再加一条join语句。例如,如果每次采购又一个或多个采购项,你可以按照如下方式联接采购项:
l
from c in customers
join p in purchases on c.ID equals p.CustomerID // first join
join pi in purchaseItems on p.ID equals pi.PurchaseID // second join
...
l purchases在第一次联接中为内部序列,而在第二次联接中则为外部序列。运用嵌套的foreach语句,也可以得到同样的结果(效率更低),代码如下:
l
foreach (Customer c in customers)
foreach (Purchase p in purchases)
if (c.ID == p.CustomerID)
foreach (PurchaseItem pi in purchaseItems)
if (p.ID == pi.PurchaseID)
Console.WriteLine (c.Name + "," + p.Price + "," + pi.Detail);
l 在查询广义语法中,先前联接中的变量一直在作用域内,这与SelectMany式查询中的外部迭代变量一样。你还可以在join语句之间插入where和let语句。
6.依据多个键进行联接
l 利用匿名类型可以依据多个键进行联接,代码如下:
l
from x in sequenceX
join y in sequenceY on new { K1 = x.Prop1, K2 = x.Prop2 }
equals new { K1 = y.Prop3, K2 = y.Prop4 }
...
l 要保证这段代码可以运行,两个匿名类型的结构必须相同。然后编译器用同一内部类型实现各个匿名类型,从而使多个联接键兼容。
7.用lambda语法进行联接
l 如下广义语法表示的联接:
l
from c in customers
join p in purchases on c.ID equals p.CustomerID
select new { c.Name, p.Description, p.Price };
l 用lambda语法表示如下:
l
customers.Join ( // outer collection
purchases, // inner collection
c => c.ID, // outer key selector
p => p.CustomerID, // inner key selector
(c, p) => new
{ c.Name, p.Description, p.Price } // result selector
);
l 最终的结果选择器表达式创建输出序列中的各个元素。如果有比投影优先级高的其他语句,如下例中的orderby:
l
from c in customers
join p in purchases on c.ID equals p.CustomerID
orderby p.Price
select c.Name + " bought a " + p.Description;
l 用lambda语法表示时,必须在结果选择器中构造一个临时的匿名类型:
l
customers.Join ( // outer collection
purchases, // inner collection
c => c.ID, // outer key selector
p => p.CustomerID, // inner key selector
(c, p) => new { c, p } ) // result selector
.OrderBy (x => x.p.Price)
.Select (x => x.c.Name + " bought a " + x.p.Description);
l 实现联接时,广义语法通常更受欢迎,因为它不需要那么多技巧。
8.GroupJoin
l GroupJoin实现的功能与Join一样,只不过它生成的不是扁平结果集,而是按照每一个外部元素分组的层次结果集。它还支持左外部联接。
l GroupJoin的广义语法与Join相同,但它后面跟随有into关键字。
l 以下是最基本的例子:
l
IEnumerable<IEnumerable<Purchase>> query =
from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
select custPurchases; // custPurchases is a sequence
l 仅当into语句紧跟在join语句后面时,into语句才转换为GroupJoin。出现在select或group语句后面时,它表示查询继续。Into关键字的两种用途十分不同,不过它们有一个共同的特性:都引入一个新的查询变量。
l
l 结果为一系列序列,我们可以对它进行枚举,代码如下:
l
foreach (IEnumerable<Purchase> purchaseSequence in query)
foreach (Purchase p in purchaseSequence)
Console.WriteLine (p.Description);
l 不过,这个用处不大,因为outerSeq没有对外部客户的引用。通常,你要在投影中引用外部迭代变量:
l
from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
select new { CustName = c.Name, custPurchases };
l 这段代码所得的结果与如下Select子查询(效率低)结果相同:
l
from c in customers
select new
{
CustName = c.Name,
custPurchases = purchases.Where (p => c.ID == p.CustomerID)
};
l 默认情况下,GroupJoin相当于左外部联接。你需要按custPurchases进行筛选来得到内部联接——排除没有采购的客户:
l
from c in customers join p in purchases on c.ID equals p.CustomerID
into custPurchases
where custPurchases.Any( )
select ...
l 分组联接into之后的语句对内部孩子元素的子序列进行运算,不对单个孩子元素进行运算。也就是说,要筛选单次采购,你必须在联接之前调用Where:
l
from c in customers
join p in purchases.Where (p2 => p2.Price > 1000)
on c.ID equals p.CustomerID
into custPurchases ...
l 你可以用GroupJoin构建lambda查询,这与Join一样。
9.扁平外部联接
l 如果你既想要外部联接,又要获得扁平结果集,那么你将陷入进退两难的局面。GroupJoin提供外部联接;Join提供扁平结果集。解决办法是,先调用GroupJoin,然后对每一个孩子序列运用DefaultIfEmpty,最后对结果运用SelectMany:
l
from c in customers
join p in purchases on c.ID equals p.CustomerID into custPurchases
from cp in custPurchases.DefaultIfEmpty( )
select new
{
CustName = c.Name,
Price = cp == null ? (decimal?) null : cp.Price
};
l 如果purchases的子序列为空,则DefaultIfEmpty输出一个空值。第二个from语句转换为SelectMany。它扩展并展平所有采购子序列,将它们联接成为一个采购元素序列。
10.带lookups的联接
l Enumerable 类中的Join和GroupJoin方法分为两步。第一步,它们将内部序列加载到一个lookup中。第二步,结合lookup对外部序列进行查询。
l Lookup即为一个分组序列,其中分组可以通过键直接访问。还可以把它理解为是序列词典,其中的每一个键下可以有多个元素。Lookups是只读的,通过以下接口定义:
l
public interface ILookup<TKey,TElement> :
IEnumerable<IGrouping<TKey,TElement>>, IEnumerable
{
int Count { get; }
bool Contains (TKey key);
IEnumerable<TElement> this [TKey key] { get; }
}
l 和其他序列输出运算符一样,联接运算符也遵从延迟执行或迟缓执行语义。也就是说,直到开始枚举输出序列时,才构建lookup。
l
l 处理本地集合时,除了用联接运算符外,你还可以手动地创建和查询lookups。这么做有几点好处:
l • 你可以在多个查询以及普通命令代码中重用同一个lookup。
l • 查询lookup是一种极好的理解Join和GroupJoin工作原理的方式。
l ToLookupTT扩展方法创建一个lookup。如下代码将所有采购加载到lookup中,以其CustomerID为键:
l
ILookup<int?,Purchase> purchLookup =
purchases.ToLookup (p => p.CustomerID, p => p);
l 第一个变量选取键;第二个变量选取对象,该对象将作为值加载到lookup中。
l 浏览lookup非常类似于浏览一本词典,只不过lookup的索引返回匹配的项目序列,而非单个匹配项目。如下代码枚举ID为1的客户的所有采购:
l
foreach (Purchase p in purchLookup [1])
Console.WriteLine (p.Description);
l 只要恰当的运用lookup,你就可以编写与Join/GroupJoin 查询一样高效的SelectMany/Select查询。Join与在lookup上运用SelectMany是等效的:
l
from c in customers
from p in purchLookup [c.ID]
select new { c.Name, p.Description, p.Price };
Tom Bike 500
Tom Holiday 2000
Dick Bike 600
Dick Phone 300
...
l 添加DefaultIfEmpty调用则使之成为一个外部联接:
l
from c in customers
from p in purchLookup [c.ID].DefaultIfEmpty( )
select new {
c.Name,
Descript = p == null ? null : p.Description,
Price = p == null ? (decimal?) null : p.Price
};
l GroupJoin与在投影内部浏览lookup是等效的:
l
from c in customers
select new {
CustName = c.Name,
CustPurchases = purchLookup [c.ID]
};
11.Enumerable类的实现
l 这是最简单的Enumerable.Join的有效实现,忽略了空检查:
l
public static IEnumerable <TResult> Join
<TOuter,TInner,TKey,TResult> (
this IEnumerable <TOuter> outer,
IEnumerable <TInner> inner,
Func <TOuter,TKey> outerKeySelector,
Func <TInner,TKey> innerKeySelector,
Func <TOuter,TInner,TResult> resultSelector)
{
ILookup <TKey, TInner> lookup = inner.ToLookup (innerKeySelector);
return
from outerItem in outer
from innerItem in lookup [outerKeySelector (outerItem)]
select resultSelector (outerItem, innerItem);
}
l GroupJoin的实现与Join相似,但更简单:
l
public static IEnumerable <TResult> GroupJoin
<TOuter,TInner,TKey,TResult> (
this IEnumerable <TOuter> outer,
IEnumerable <TInner> inner,
Func <TOuter,TKey> outerKeySelector,
Func <TInner,TKey> innerKeySelector,
Func <TOuter,IEnumerable<TInner>,TResult> resultSelector)
{
ILookup <TKey, TInner> lookup = inner.ToLookup (innerKeySelector);
return
from outerItem in outer
select resultSelector
(outerItem, lookup [outerKeySelector (outerItem)]);
}
9.5 排序
IEnumerable<TSource> à IOrderedEnumerable<TSource>
方法 |
描述 |
等价的SQL方法 |
OrderBy, ThenBy |
按升序对序列进行排序 |
ORDER BY ... |
OrderByDescending, ThenByDescending |
按降序对序列进行排序 |
ORDER BY ... DESC |
Reverse |
按逆序返回序列 |
抛出异常 |
l 排序运算符按不同的顺序返回相同的元素。
l
9.5.1 OrderBy,OrderByDescending,ThenBy和ThenByDescending
1.OrderBy和OrderByDescending的变量
参数 |
类型 |
输入序列 |
IEnumerable<TSource> |
键选择器 |
TSource => TKey |
l 返回类型 = IOrderedEnumerable<TSource>
2.ThenBy和ThenByDescending的参数
参数 |
类型 |
输入序列 |
IOrderedEnumerable<TSource> |
键选择器 |
TSource => TKey |
3.广义语法
orderby expression1 [descending] [, expression2 [descending] ... ]
4.概述
l OrderBy返回排序后的输入序列,使用keySelector表达式进行比较。如下查询按字母顺序输出名字序列:
l
IEnumerable<string> query = names.OrderBy (s => s);
l 如下查询按长度对名字进行排序:
l
IEnumerable<string> query = names.OrderBy (s => s.Length);
// Result: { "Jay", "Tom", "Mary", "Dick", "Harry" };
l 具有同样排序键值的元素(本例中的Jay/Tom和Mary/Dick),其相对顺序是不确定的,除非添加一个ThenBy运算符:
l
IEnumerable<string> query = names.OrderBy (s => s.Length).ThenBy (s => s);
// Result: { "Jay", "Tom", "Dick", "Mary", "Harry" };
l ThenBy只对在前一次排序中具有相同排序键值的元素进行重新排序。你可以串接任意个ThenBy运算符。下例先按长度排序,然后按第二个字符顺序排序,最后按第一个字符顺序排序:
l
names.OrderBy (s => s.Length).ThenBy (s => s[1]).ThenBy (s => s[0]);
l 用广义语法表达的等价查询如下:
l
from s in names
orderby s.Length, s[1], s[0]
select s;
l LINQ 还提供了OrderByDescending和ThenByDescending运算符,它们实现相同的功能,即按逆序输出结果。下面这个LINQ to SQL查询按价格的降序检索采购,对于价格相同的则按字母顺序列出:
l
dataContext.Purchases.OrderByDescending (p => p.Price)
.ThenBy (p => p.Description);
In comprehension syntax:
from p in dataContext.Purchases
orderby p.Price descending, p.Description
select p;
5.比较器和校正
l 本地查询中,键选择器对象通过其默认的IComparable实现(见第7章)决定排序算法。你可以通过传入IComparer对象来覆盖排序算法。如下语句执行不区分大小写的排序:
l
names.OrderBy (n => n, StringComparer.CurrentCultureIgnoreCase);
l 广义语法和LINQ to SQL均不支持传入比较器。在LINQ to SQL中,比较算法是由参与栏的collation决定的。如果collation区分大小写,那么你可以通过在键选择器中调用ToUpper来要求执行不区分大小写的排序:
l
from p in dataContext.Purchases
orderby p.Description.ToUpper( )
select p;
IOrderedEnumerable and IOrderedQueryable
l 排序运算符返回IEnumerable<T>的特定子类型。Enumerable类中的运算符返回IOrderedEnumerable类型;Queryable类中的运算符则返回IOrderedQueryable类型。这些子类型允许随后运用ThenBy运算符来完善查询,但并不是取代已有查询。
l 这些子类型定义的附加成员并没有公开,因此它们和普通的序列表现相似。当逐步构建查询时,它们的与众不同才显现出来:
l
IOrderedEnumerable<string> query1 = names.OrderBy (s => s.Length);
IOrderedEnumerable<string> query2 = query1.ThenBy (s => s);
l 如果我们改为声明query1为IEnumerable<string>类型,第二行则无法编译——ThenBy需要类型为IOrderedEnumerable<string>的输入。你可以通过隐式定义查询变量来免除这一担心:
l
var query1 = names.OrderBy (s => s.Length);
var query2 = query1.ThenBy (s => s);
l 隐式输入也会产生它自己的问题。如下代码就无法编译:
l
var query = names.OrderBy (s => s.Length);
query = query.Where (n => n.Length > 3); // Compile-time error
l 基于OrderBy运算符的输出序列类型,编译器推断查询为IOrderedEnumerable<string>类型。然而,下一行的Where运算符返回普通的IEnumerable<string>类型,该类型不能再赋给查询。你可以通过显示类型解决这一问题,也可以通过在OrderBy之后调用AsEnumerable( ):
l
var query = names.OrderBy (s => s.Length).AsEnumerable( );
query = query.Where (n => n.Length > 3); // OK
l 在解释查询中,则是调用AsQueryable。
9.6 分组
IEnumerable<TSource> à IEnumerable<IGrouping<TSource,TElement>>
方法 |
描述 |
等价的SQL方法 |
GroupBy |
将一个序列分组成多个子序列 |
GROUP BY |
9.6.1 GroupBy
参数 |
类型 |
输入序列 |
IEnumerable<TSource> |
键选择器 |
TSource => TKey |
元素选择器(可选) |
TSource => TElement |
比较器(可选) |
IEqualityComparer<TKey> |
1.广义语法
group element-expression by key-expression
2.概述
l GroupBy将一个扁平输入序列转变为组序列。例如,以下代码通过扩展组织c:\temp目录下的所有文件:
l
string[] files = Directory.GetFiles ("c:\\temp");
IEnumerable<IGrouping<string,string>> query =
files.GroupBy (file => Path.GetExtension (file));
l 或者也许你更愿意用隐式类型:
l
var query = files.GroupBy (file => Path.GetExtension (file));
l 如下代码枚举结果:
l
foreach (IGrouping<string,string> grouping in query)
{
Console.WriteLine ("Extension: " + grouping.Key);
foreach (string filename in grouping)
Console.WriteLine (" - " + filename);
}
Extension: .pdf
-- chapter03.pdf
-- chapter04.pdf
Extension: .doc
-- todo.doc
-- menu.doc
-- Copy of menu.doc
...
l Enumerable.GroupBy将输入元素读入一个临时的列表字典,从而具有相同键值的所有元素会在同一个子列表中。然后输出分组序列。一个分组就是具有一个Key属性的序列:
l
public interface IGrouping <TKey,TElement> : IEnumerable<TElement>,
IEnumerable
{
TKey Key { get; } // Key applies to the subsequence as a whole
}
l 默认情况下,每个分组中的元素都是未经转换的输入元素,除非你定义了elementSelector参数。下面的代码将每个输入元素都投影为大写字母:
l
files.GroupBy (file => Path.GetExtension (file), file => file.ToUpper( ));
l elementSelector与keySelector无关。在我们的例子中,就意味着每个分组的Key仍然是其原来的形式:
l
Extension: .pdf
-- CHAPTER03.PDF
-- CHAPTER04.PDF
Extension: .doc
-- TODO.DOC
l 注意,子集合并不是按照键值的字母顺序输出的。GroupBy只进行分组,并不排序;事实上,它保留了原始顺序。要实现排序,必须添加OrderBy运算符:
l
files.GroupBy (file => Path.GetExtension (file), file => file.ToUpper( ))
.OrderBy (grouping => grouping.Key);
l 在广义语法中,GroupBy有一个简单且直接的转换:
l
group element-expr by key-expr
l 我们的例子用广义语法表示如下:
l
from file in files
group file.ToUpper( ) by Path.GetExtension (file);
l 与select一样,group结束一个查询——除非添加了查询延续语句:
l
from file in files
group file.ToUpper( ) by Path.GetExtension (file) into grouping
orderby grouping.Key
select grouping;
l 查询延续在group by查询中通常很有用。下一个查询筛选出少于五个文件的分组:
l
from file in files
group file.ToUpper( ) by Path.GetExtension (file) into grouping
where grouping.Count( ) < 5
select grouping;
l group by之后的where等价于SQL中的HAVING。它作用于每一个子序列或分组,不是单独作用于个别元素。
l
l 有时,你只对一次分组的聚合结果感兴趣,因此可以放弃子序列:
l
string[] votes = { "Bush", "Gore", "Gore", "Bush", "Bush" };
IEnumerable<string> query = from vote in votes
group vote by vote into g
orderby g.Count( ) descending
select g.Key;
string winner = query.First( ); // Bush
3.LINQ to SQL中的GroupBy
l 在解释查询中,分组的工作原理也是一样的。如果你已经在LINQ to SQL中建立了关联属性,你就会发现,相比标准的SQL而言,其分组的需求要少得多。例如,要选取至少有两次采购的客户,你不需要进行分组;如下查询就能很好的完成这一任务:
l
from c in dataContext.Customers
where c.Purchases.Count >= 2
select c.Name + " has made " + c.Purchases.Count + " purchases";
l 你可以要是用分组的一个例子,就是要按年列出总的销售额:
l
from p in dataContext.Purchases
group p.Price by p.Date.Year into salesByYear
select new {
Year = salesByYear.Key,
TotalValue = salesByYear.Sum( )
};
l 从功能上看,LINQ的分组运算符暴露出SQL的“GROUP BY”的超集。
l 另一偏离传统SQL的方面在于,LINQ中没有任何义务对用在分组或排序当中的变量或表达式进行投影。
4.依据多个键进行分组
l 你可以依据复合键进行分组,运用一个匿名类型:
l
from n in names
group n by new { FirstLetter = n[0], Length = n.Length };
5.定制的相等比较器
l 在本地查询中,你可以将一个定制的相等比较器传给GroupBy,以改变键值比较的算法。不过这一点很少需要用到,因为改变键选择器表达式通常就足够了。例如,如下代码创建一个不区分大小写的分组:
l
group name by name.ToUpper( )
9.7 Set运算符
IEnumerable<TSource>, IEnumerable<TSource> à IEnumerable<TSource>
方法 |
描述 |
等价的SQL方法 |
Concat |
返回两个序列中元素的串联结果集合 |
UNION ALL |
Union |
返回两个序列中元素的串联结果集合,去除了重复的元素 |
UNION |
Intersect |
返回两个序列中都存在的元素 |
WHERE ... IN (...) |
Except |
返回第一个序列中存在而第二个序列不存在的元素 |
EXCEPT 或 WHERE ... NOT IN (...) |
9.7.1 Concat和Union
l Concat返回第一个序列中的所有元素,其后紧跟着第二个序列的所有元素。Union所实现的功能与此相同,不过它去除了重复的元素:
l
int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };
IEnumerable<int>
concat = seq1.Concat (seq2), // { 1, 2, 3, 3, 4, 5 }
union = seq1.Union (seq2); // { 1, 2, 3, 4, 5 }
9.7.2 Intersect和Except
l Intersect返回两个序列共有的元素。Except返回第一个输入序列中有而第二个输入序列中没有的元素:
l
int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };
IEnumerable<int>
commonality = seq1.Intersect (seq2), // { 3 }
difference1 = seq1.Except (seq2), // { 1, 2 }
difference2 = seq2.Except (seq1); // { 4, 5 }
l Enumerable.Except的实现原理是:将第一个集合中的所有元素载入一个字典,然后从字典中删除所有在第二个序列中存在的元素。SQL中的等价方法是NOT EXISTS或NOT IN子查询:
l
SELECT number FROM numbers1Table
WHERE number NOT IN (SELECT number FROM numbers2Table)
9.8 转换方法
l LINQ主要处理序列,即IEnumerable<T>类型的集合。转换方法将序列转换为其他类型的集合,或者将其他类型的集合转换为序列:
l
方法 |
描述 |
OfType |
将IEnumerable转换为IEnumerable<T>类型,舍弃类型错误的元素 |
Cast |
将IEnumerable转换为IEnumerable<T>类型,如果有类型错误的元素则抛出异常 |
ToArray |
将IEnumerable<T>转换为List<T>类型 |
ToList |
将IEnumerable<T>转换为T[]类型 |
ToDictionary |
将IEnumerable<T>转换为Dictionary<TKey,TValue>类型 |
ToLookup |
将IEnumerable<T>转换为ILookup<TKey,TElement>类型 |
AsEnumerable |
向下转换为IEnumerable<T> |
AsQueryable |
转换为IQueryable<T>类型 |
9.8.1 OfType和Cast
l OfType和Cast接收一个泛型IEnumerable集合,输出一个泛型IEnumerable<T>序列,之后你可以对其进行枚举:
l
ArrayList classicList = new ArrayList( ); // in System.Collections
classicList.AddRange ( new int[] { 3, 4, 5 } );
IEnumerable<int> sequence1 = classicList.Cast<int>( );
l 当遇到输入元素的类型不兼容时,Cast和OfType的行为有所不同。Cast抛出异常;而OfType忽略不兼容的元素。承接前面的例子:
l
DateTime offender = DateTime.Now;
classicList.Add (offender);
IEnumerable<int>
sequence2 = classicList.OfType<int>( ), // OK - ignores offending DateTime
sequence3 = classicList.Cast<int>( ); // Throws exception
l 运算符严格遵从C#的元素兼容性规则。我们可以通过查看OfType的内部实现来看看:
l
public static IEnumerable<TSource> OfType <TSource> (IEnumerable source)
{
foreach (object element in source)
if (element is TSource)
yield return (TSource)element;
}
l Cast的实现是一样的,只不过少了类型兼容性测试:
l
public static IEnumerable<TSource> Cast <TSource> (IEnumerable source)
{
foreach (object element in source)
yield return (TSource)element;
}
l 由上面方法的实现可以得出一个结论:不可以用Cast将元素从一种值类型转换为另一种值类型(要实现这一功能,必须执行Select操作)。也就是说,Cast不如C#的cast运算符灵活,C#的cast运算符支持静态类型转换,如下例:
l
int i = 3;
long l = i; // Static conversion int->long
int i2 = (int) l; // Static conversion long->int
l 我们可以试验一下,尝试用OfType或Cast将int型序列转换为long型序列:
l
int[] integers = { 1, 2, 3 };
IEnumerable<long> test1 = integers.OfType<long>( );
IEnumerable<long> test2 = integers.Cast<long>( );
l 枚举时,test1输出零个元素,而test2抛出异常。查看OfType方法的实现,原因就相当明了了。替换TSource后,我们得到如下表达式:
l
(element is long)
l 因为没有继承关系,对于int型元素,它返回false。
l
l 枚举时,test2抛出异常的原因则不是十分明显。注意,在Cast方法的实现中,元素为object类型。当TSource为值类型时,CLR会合成一个方法来再现这一情景,我们在第三章的“
l
int value = 123;
object element = value;
long result = (long) element; // exception
l 因为元素变量声明为object类型,执行的是object到long的转换(拆箱),而非int到long的数值转换。拆箱操作需要准确的类型匹配,因此当给int类型时object到long的猜想操作就会失败。
l
l 和我们前面建议的一样,用Select方法就可以了:
l
IEnumerable<long> castLong = integers.Select (s => (long) s);
l 在向下转换泛型序列中的元素方面,OfType和Cast也十分有用。例如,如果你的输入序列为IEnumerable<Fruit>类型,OfType<Apple>就只返回苹果。这一特性在LINQ to XML(见第10章)中特别有用。
9.8.2 ToArray,ToList,ToDictionary和ToLookup
l ToArray和ToList将结果输出到数组或泛型列表中。这些运算符会导致输入序列的枚举立即执行(除非间接通过子查询或表达式树)。示例参考第8章的“8.4 延迟执行”一节。
l ToDictionary和ToLookup接受如下参数:
l
参数 |
类型 |
输入序列 |
IEnumerable<TSource> |
键选择器 |
TSource => TKey |
元素选择器(可选) |
TSource => TElement |
比较器(可选) |
IEqualityComparer<TKey> |
l ToDictionary也会导致序列的枚举立即执行,将结果写入泛型Dictionary。你提供的键选择器表达式对输入序列中的每个元素求值必须是唯一的;否则,就会抛出异常。相反,ToLookup允许多个元素的键值相同。我们在
9.8.3 A sEnumerable和AsQueryable
l AsEnumerable将一个序列向上转换为IEnumerable<T>类型,使得编译器将随后的查询运算符绑定到Enumerable类中的方法,而不会绑定到Queryable类中的方法。示例见第8章的“
l 如果序列实现了IQueryable<T>接口,AsQueryable则将其向下转换为IQueryable<T>类型。否则,就实例化一个在本地查询之上的IQueryable<T>封装器。
9.9 元素运算符
IEnumerable<TSource> à TSource
方法 |
描述 |
等价的SQL方法 |
First,FirstOrDefault |
返回序列中的第一个元素,可选满足某一谓词的第一个元素 |
SELECT TOP 1 ... ORDER BY ... |
Last,LastOrDefault |
返回序列中的最后一个元素,可选满足某一谓词的最后一个元素 |
SELECT TOP 1 ... ORDER BY ... DESC |
Single,SingleOrDefault |
与First/FirstOrDefault等价,但是当在多个元素匹配时会抛出异常 |
|
ElementAt,ElementAtOrDefaul |
返回指定位置上的元素 |
抛出异常 |
DefaultIfEmpty |
如果序列中没有元素,则返回空或default(TSource) |
OUTER JOIN |
l 如果输入序列为空或者没有元素匹配给定的谓词,名字以“OrDefault”结束的方法返回default(TSource),不会抛出异常。
l 对于引用类型元素,default(TSource) = null;对于值类型元素则为“blank”(通常为0)。
9.9.1 First,Last和Single
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
谓词(可选) |
TSource => bool |
l 如下示例演示了First和Last方法:
l
int[] numbers = { 1, 2, 3, 4, 5 };
int first = numbers.First( ); // 1
int last = numbers.Last( ); // 5
int firstEven = numbers.First (n => n % 2 == 0); // 2
int lastEven = numbers.Last (n => n % 2 == 0); // 4
l 如下示例对比First和FirstOrDefault:
l
int firstBigError = numbers.First (n => n > 10); // Exception
int firstBigNumber = numbers.FirstOrDefault (n => n > 10); // 0
l 为避免出现异常,Single只需要一个匹配元素;SingleOrDefault需要一个或零个匹配元素:
l
int onlyDivBy3 = numbers.Single (n => n % 3 == 0); // 3
int divBy2Err = numbers.Single (n => n % 2 == 0); // Error: 2 & 4 match
int singleError = numbers.Single (n => n > 10); // Error
int noMatches = numbers.SingleOrDefault (n => n > 10); // 0
int divBy2Error = numbers.SingleOrDefault (n => n % 2 == 0); // Error
l Single是元素运算符家族中最麻烦的一个。而FirstOrDefault和LastOrDefaul是最宽容的。
l 在LINQ to SQL中,Single常用来通过主键检索表中的一行数据:
l
Customer cust = dataContext.Customers.Single (c => c.ID == 3);
9.9.2 ElementAt
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
返回元素的索引 |
int |
l ElementAt选取序列中的第n个元素:
l
int[] numbers = { 1, 2, 3, 4, 5 };
int third = numbers.ElementAt (2); // 3
int tenthError = numbers.ElementAt (9); // Exception
int tenth = numbers.ElementAtOrDefault (9); // 0
l Enumerable.ElementAt是这样编写的:如果输入序列恰好实现IList<T>接口,它就调用IList<T>的索引器。否则,它就枚举n次,然后返回下一个元素。LINQ to SQL 不支持ElementAt方法。
9.9.3 DefaultIfEmpty
l DefaultIfEmpty将空序列转换为空/ default( )。它用在了编写扁平外部联接中:见前面的
9.10 聚合方法
IEnumerable<TSource> à scalar
方法 |
描述 |
等价的SQL方法 |
Count, LongCount |
返回输入序列中的元素个数,可选满足某个谓词的元素个数 |
COUNT (...) |
Min,Max |
返回输入序列中最小或最大的元素 |
MIN (...), MAX (...) |
Sum,Average |
计算序列中元素的数值总和或平均值 |
SUM (...), AVG (...) |
Aggregate |
执行某个定制的聚合 |
抛出异常 |
9.10.1 Count和LongCount
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
谓词(可选) |
TSource => bool |
l Count只是枚举序列,返回其元素个数:
l
int fullCount = new int[] { 5, 6, 7 }.Count( ); // 3
l Enumerable.Count的内部实现对输入序列进行了判断,看看它是否正好实现ICollection<T>接口。如果是的话,就只需调用ICollection<T>.Count。否则,就枚举每个元素,计数器进行累加。
l 你可以选择提供一个谓词:
l
int digitCount = "pa55w0rd".Count (c => char.IsDigit (c)); // 3
l LongCount与Count实现的功能相同,不过它返回64位的整型值,从而支持元素个数大于20亿的序列。
9.10.2 Min和Max
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
结果选择器(可选) |
TSource => TResult |
l Min和Max返回序列中最小或最大的元素:
l
int[] numbers = { 28, 32, 14 };
int smallest = numbers.Min( ); // 14;
int largest = numbers.Max( ); // 32;
l 如果包含有结果选择器表达式,那么元素会先进行投影:
l
int smallest = numbers.Max (n => n % 10); // 8;
l 如果元素本身并非可比的,结果选择器会进行强制类型转换。也就是说,如果元素没有实现IComparable<T>接口:
l
Purchase runtimeError = dataContext.Purchases.Min ( ); // Error
decimal? lowestPrice = dataContext.Purchases.Min (p => p.Price); // OK
l 结果选择器不仅决定着元素如何进行比较,还决定着最终的结果。在前面的例子中,最终结果为数值类型,而非采购对象。要获得最便宜的采购,需要添加一个子查询:
l
Purchase cheapest = dataContext.Purchases
.Where (p => p.Price == dataContext.Purchases.Min (p2 => p2.Price))
.FirstOrDefault( );
l 在这个例子中,你也可以不用聚合方法来表示这个查询——运用OrderBy之后再用FirstOrDefault。
9.10.3 Sum和Average
参数 |
类型 |
源序列 |
IEnumerable<TSource> |
结果选择器(可选) |
TSource => TResult |
l Sum和Average的运用方式与Min和Max相似:
l
decimal[] numbers = { 3, 4, 8 };
decimal sumTotal = numbers.Sum( ); // 15
decimal average = numbers.Average( ); // 5 (mean value)
l 如下代码返回名字数组中各个字符串长度的总和:
l
int combinedLength = names.Sum (s => s.Length); // 19
l Sum和Average在类型方面相当受限。它们的定义将其可以操作的类型限制为数值类型(int,long,float,double,decimal及其相应的可以为空的版本)。相反,Min和Max可以操作任何实现了IComparable<T>接口的类型,例如字符串。
l 此外,依据下表,Average总是返回decimal或double这两种类型之一:
l
选择器类型 |
结果类型 |
decimal |
decimal |
int, long, float, double |
double |
l 因此,如下的代码无法编译(“不能将double类型转换为int类型”):
l
int avg = new int[] { 3, 4 }.Average( );
But this will compile:
double avg = new int[] { 3, 4 }.Average( ); // 3.5
l Average暗中升级了输入值,以避免损失精度。在这个例子中,我们对整数求平均值,无需采取输入元素转换,而得到了结果3.5:
l
double avg = numbers.Average (n => (double) n);
l 在LINQ to SQL中,Sum和Average转换为标准的SQL聚合方法。如下查询返回平均采购值超过$500的客户:
l
from c in dataContext.Customers
where c.Purchases.Average (p => p.Price) > 500
select c.Name;
9.10.4 A ggregate
l Aggregate支持添加定制的累加算法,用于实现特殊的聚合。LINQ to SQL 并不支持Aggregate。Aggregate有其专攻的用例。如下代码演示了Aggregate如何实现Sum的功能:
l
int[] numbers = { 1, 2, 3 };
int sum = numbers.Aggregate (0, (seed, n) => seed + n); // 6
l Aggregate的第一个参数是种子值(seed),累加由此开始。第二个参数是用于更新累加值的表达式,需给定一个新的元素。你可以选择提供第三个参数,用于将累加值投影到最终结果值。
l Aggregate的难点在于,简单的标量类型很少担当有效的累加器职能。例如,要计算平均值,你需要同时保留元素个数的计数以及元素的总和。编写定制的累加器类型可以解决这一问题,但是相对传统的运用简单的foreach循环来计算聚合的方法而言,这种方法需要投入更多精力。
9.11 限定符
IEnumerable<TSource> à bool
方法 |
描述 |
等价的SQL方法 |
Contains |
如果输入序列包含指定的元素,则返回true |
WHERE ... IN (...) |
Any |
如果存在任何一个元素满足指定的谓词,则返回true |
WHERE ... IN (...) |
All |
如果所有元素都满足指定的谓词,则返回true |
WHERE (...) |
SequenceEqual |
如果第二个序列含有与输入序列完全相同的元素,则返回true |
|
9.11.1 Contains和Any
l Contains方法接收类型为TSource的参数;Any接收一个可选的谓词。
l 如果指定的元素存在,则Contains返回true:
l
bool hasAThree = new int[] { 2, 3, 4 }.Contains (3); // true;
l 如果指定的表达式对至少一个元素为真,则Any返回true。我们可以用Any重写前面的查询,代码如下:
l
bool hasAThree = new int[] { 2, 3, 4 }.Any (n => n == 3); // true;
l Any能够实现Contains所能实现的全部功能,甚至更多:
l
bool hasABigNumber = new int[] { 2, 3, 4 }.Any (n => n > 10); // false;
l 如果序列有一个或多个元素,调用无谓词的Any会返回true。编写前面那个查询的另一种方式如下:
l
bool hasABigNumber = new int[] { 2, 3, 4 }.Where (n => n > 10).Any( );
l Any在子查询中特别有用。
9.11.2 A ll和SequenceEqual
l 如果所有元素都满足指定的谓词,All返回true。如下代码返回采购少于$100的客户:
dataContext.Customers.Where (c => c.Purchases.All (p => p.Price < 100));
l SequenceEqual比较两个序列。只有当两个序列的元素完全相同,且顺序也相同时,才返回true。
9.12 生成方法
void à IEnumerable<TResult>
方法 |
描述 |
Empty |
生成一个空序列 |
Repeat |
生成一个包含重复元素的序列 |
Range |
生成一个整数序列 |
l Empty、Repeat和Range是静态方法(非扩展方法),用来生成简单的本地序列。
9.12.1 Empty
l Empty生成一个空序列,它只需一个类型参数:
l
foreach (string s in Enumerable.Empty<string>( ))
Console.Write (s); // <nothing>
l 与??运算符联合使用,Empty可以实现DefaultIfEmpty的相反功能。例如,假设我们有一个外形参差不齐的整数数组,我们希望让所有的整数变成一个扁平列表。如果任一内层数组为空,则如下的SelectMany查询会失败:
l
int[][] numbers =
{
new int[] { 1, 2, 3 },
new int[] { 4, 5, 6 },
null // this null makes the query below fail.
};
IEnumerable<int> flat = numbers.SelectMany (innerArray => innerArray);
l Empty与??的联合能够克服这一问题:
l
IEnumerable<int> flat = numbers
.SelectMany (innerArray => innerArray ?? Enumerable.Empty <int>( ) );
foreach (int i in flat)
Console.Write (i + " "); // 1 2 3 4 5 6
9.12.2 Range和Repeat
l Range和Repeat只对整数起作用。Range接收一个起始索引和计数:
l
foreach (int i in Enumerable.Range (5, 5))
Console.Write (i + " "); // 5 6 7 8 9
l Repeat接收重复的数值以及迭代的次数:
l
foreach (int i in Enumerable.Repeat (5, 3))
Console.Write (i + " "); // 5 5 5