LINQ之路14:LINQ Operators之排序和分组(Ordering and Grouping)

时间:2024-04-28 10:38:30

本篇继续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[]).ThenBy (s => s[]); // 对应的查询表达式语法为
IEnumerable<string> query =
from s in names
orderby s.Length, s[], s[]
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 > ); // 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 > ); // 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() <
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 >=
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)。