LINQ之路14:LINQ Operators之排序和分组(Ordering and Grouping)
本篇继续LINQ Operators的介绍,这里要讨论的是LINQ中的排序和分组功能。LINQ的排序操作符有:OrderBy, OrderByDescending, ThenBy, 和ThenByDescending,他们返回input sequence的排序版本。分组操作符GroupBy把一个平展的输入sequence进行分组存放到输出sequence中。
排序/Ordering
IEnumerable<TSource>→IOrderedEnumerable<TSource>
Operator |
说明 |
SQL语义 |
OrderBy, ThenBy |
对一个sequence按升序排序 |
ORDER BY ... |
OrderByDescending, ThenByDescending |
对一个sequence按降序排序 |
ORDER BY ... DESC |
Reverse |
按倒序返回一个sequence |
Exception thrown |
排序操作符以不同顺序返回相同的elements。
OrderBy, OrderByDescending, ThenBy, 和ThenByDescending
OrderBy和OrderByDescending的参数
参数 |
类型 |
Input sequence |
IEnumerable<TSource> |
键选择器/Key selector |
TSource => TKey
|
Return type = IOrderedEnumerable<TSource>
ThenBy和ThenByDescending参数
参数 |
类型 |
Input sequence |
IOrderedEnumerable <TSource> |
键选择器/Key selector |
TSource => TKey
|
查询表达式语法
orderby expression1 [descending] [, expression2 [descending] ... ]
简介
OrderBy返回input sequence的排序版本,使用键选择器来进行排序比较。请看下面的示例:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
//对names sequence按字母顺序排序:
IEnumerable<string> query = names.OrderBy (s => s);
// Result: { "Dick", "Harry", "Jay", "Mary", "Tom" };
//按姓名长度进行排序
IEnumerable<string> query = names.OrderBy (s => s.Length);
// Result: { "Jay", "Tom", "Mary", "Dick", "Harry" };
对于拥有相同排序键值的elements来说,他们的相对位置是不确定的,比如上例按姓名长度排序的查询,Jay和Tom、Mary和Dick,除非我们添加额外的ThenBy运算符:
IEnumerable<string> query = names.OrderBy(s => s.Length).ThenBy(s => s);
// Result: { "Jay", "Tom", "Dick", "Mary", "Harry" };
ThenBy只会对那些在前一次排序中拥有相同键值的elements进行重新排序,我们可以连接任意数量的ThenBy运算符:
// 先按长度排序,然后按第二个字符排序,再按第一个字符排序
IEnumerable<string> query =
names.OrderBy (s => s.Length).ThenBy (s => s[1]).ThenBy (s => s[0]);
// 对应的查询表达式语法为
IEnumerable<string> query =
from s in names
orderby s.Length, s[1], s[0]
select s;
LINQ也提供了OrderByDescending和ThenByDescending运算符,用来按降序排列一个sequence。下面的LINQ-to-db查询获取的purchases先按price降序排列,对于相同的price则按Description字母顺序排列:
var query = dataContext.Purchases.OrderByDescending (p => p.Price)
.ThenBy (p => p.Description);
// 查询表达式语法
var query = from p in dataContext.Purchases
orderby p.Price descending, p.Description
select p;
比较器(Comparers)和排序规则(collations)
对一个本地查询,键选择器对象本身通过其默认的IComparable实现决定了排序算法,我们可以传入一个IComparer对象来重载该排序算法。
// 排序时忽略大小写
names.OrderBy (n => n, StringComparer.CurrentCultureIgnoreCase);
查询表达式语法并不支持传入comparer的做法,LINQ to SQL和EF也没有任何方式来支持此功能。当我们查询一个数据库时,排序算法由排序列的collation(排序规则)决定。如果collation是大小写敏感的,我们可以通过在键选择器上调用ToUpper来获得忽略大小写的排序:
var query = from p in dataContext.Purchases
orderby p.Description.ToUpper()
select p;
IOrderedEnumerable和IOrderedQueryable
排序运算符 返回IEnumerable<T>的一个特殊子类型。Enumerable中的排序运算符返回
IOrderedEnumerable;Queryable中的排序运算符返回IOrderedQueryable。这些子类型允许随后的ThenBy运算符来进一步调整现有的排序,他们中定义的其他成员并没有对用户公开,所以他们看起来就像普通的sequence。仅当我们渐进的创建查询时他们的区别才会显现出来:
IOrderedEnumerable<string> query1 = names.OrderBy(s => s.Length);
IOrderedEnumerable<string> query2 = query1.ThenBy(s => s);
如果我们使用IEnumerable<string>来声明query1,第二行就会编译错误,因为ThenBy需要一个IOrderedEnumerable<string>的输入类型。我们可以通过隐式类型变量来避免这种错误:
var query1 = names.OrderBy(s => s.Length);
var query2 = query1.ThenBy(s => s);
尽管如此,隐式类型有时候也会有其自身的问题,比如下面的查询就不能编译:
var query = names.OrderBy(s => s.Length);
query = query.Where(n => n.Length > 3); // Compile-time error
基于OrderBy的输出类型,编译器推断出query的类型为IOrderedEnumerable<string>。但是下一行中的Where返回一个正常的IEnumerable<string>,所以它已不能重新赋值给query了。我们可以通过显示类型定义或在OrderBy之后调用AsEnumerable()来作为一种变通的方案:
var query = names.OrderBy(s => s.Length).AsEnumerable();
query = query.Where(n => n.Length > 3); // OK
相应的,针对解释查询,我们需要调用AsQueryable。
分组/Grouping
IEnumerable<TSource>→IEnumerable<IGrouping<TSource,TElement>>
Operator |
说明 |
SQL语义 |
GroupBy |
对一个sequence进行分组 |
GROUP BY |
GroupBy
参数 |
类型 |
Input sequence |
IEnumerable<TSource> |
键选择器/Key selector |
TSource => TKey |
元素选择器/Element selector(optional) |
TSource => TElement |
比较器/Comparer (optional) |
IEqualityComparer<TKey> |
查询表达式语法
group element-expression by key-expression
简介
GroupBy把一个平展的输入sequence进行分组存放到输出sequence中,比如下面的示例对C:\temp目录下的文件按扩展名进行分组:
string[] files = Directory.GetFiles("c:\\temp");
IEnumerable<IGrouping<string, string>> query =
files.GroupBy(file => Path.GetExtension(file));
// 使用匿名类型来存储结果
var query2 = files.GroupBy(file => Path.GetExtension(file));
// 遍历结果的方式
foreach (IGrouping<string, string> grouping in query)
{
Console.WriteLine("Extension: " + grouping.Key);
foreach (string filename in grouping)
Console.WriteLine(" - " + filename);
}
// Result:
Extension: .pdf
- chapter03.pdf
- chapter04.pdf
Extension: .doc
- todo.doc
- menu.doc
- Copy of menu.doc
...
Enumerable.GroupBy会读取每一个输入element,把他们存放到一个临时的列表dictionary,所有具有相同key的元素会被存入同一个子列表。然后返回一个分组(grouping)sequence,一个分组是一个带有Key属性的sequence:
public interface IGrouping<TKey, TElement>
: IEnumerable<TElement>, IEnumerable
{
TKey Key { get; } // 一个subsequence共享一个Key属性
}
默认情况下,每个分组里面的element都是没有经过转换的输入element,除非你指定了元素选择器参数。下面就把输入element转换到大写形式:
var query3 = files.GroupBy(
file => Path.GetExtension(file),
file => file.ToUpper());
元素选择器和键值选择器是互相独立的两个概念,上面的例子中,尽管分组中的元素是大写的,但是分组中的Key保持原来的大小写形式:
// Result:
Extension: .pdf
- chapter03.PDF
- chapter04.PDF
Extension: .doc
- todo.DOC
- menu.DOC
- Copy of menu.DOC
...
值得注意的是,分组中的子集合并没有进行排序的功能,他会保持原来的顺序。如果需要对结果排序,我们需要添加OrderBy运算符:
files.GroupBy(file => Path.GetExtension(file), file => file.ToUpper())
.OrderBy(grouping => grouping.Key);
GroupBy的查询表达式语法非常的简单和直接:group element-expression by key-expression
下面使用查询表达式重写上面的例子:
var query =
from file in files
group file.ToUpper() by Path.GetExtension(file);
和select一样,group也会结束一个查询,除非我们增加了一个可以继续查询的子句:
var query =
from file in files
group file.ToUpper() by Path.GetExtension(file) into grouping
orderby grouping.Key
select grouping;
续写查询对于group by运算符来说非常有用,因为我们很可能要对分组进行过滤等操作。
// 只选择元素数量小于3的分组
var query =
from file in files
group file.ToUpper() by Path.GetExtension(file) into grouping
where grouping.Count() < 3
select grouping;
group by之后的where子句相当于SQL中的HAVING,它会应用到整个分组或subsequence,而不是单个元素。
有时候,我们可能仅对分组的汇总感兴趣,所以我们可以丢弃subsequence:
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
LINQ to SQL和EF中的GroupBy
Grouping在对数据库进行查询时其工作方式是一样的。但是如果设置了关联属性,你会发现group的使用几率不会像标准SQL中那么频繁,因为关联属性已经为我们实现了特定的分组功能。例如,我们想要选择至少有 两个purchases的customers,我们并不需要分组,下面的查询就可以工作得很好:
var query =
from c in dataContext.Customers
where c.Purchases.Count >= 2
select c.Name + " has made " + c.Purchases.Count + " purchases";
下面是一个使用group的例子:
// 对销售额按年份分组
var query = from p in dataContext.Purchases
group p.Price by p.Date.Year into salesByYear
select new {
Year = salesByYear.Key,
TotalValue = salesByYear.Sum()
};
按多键值分组
我们可以按一个复合键值进行分组,方式是使用一个匿名类型来表示这个键值:
// 对purchase按年月分组
var query = from p in dataContext.Purchases
group p by new { Year = p.Date.Year, Month = p.Date.Month };
至此,LINQ Operators我们已经介绍了过滤、数据转换、连接、排序和分组。关于LINQ Operators,在接下来的最后两篇中,会讨论其他还没讲述的运算符,包括:Set、Zip、转换方法、Element运算符、集合方法、量词(Quantifiers)生成方法(Generation Methods)。