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)。

时间: 2024-10-24 01:02:46

LINQ之路14:LINQ Operators之排序和分组(Ordering and Grouping)的相关文章

LINQ之路 7:子查询、创建策略和数据转换

在前面的系列中,我们已经讨论了LINQ简单查询的大部分特性,了解了LINQ的支持计术和语法形式.至此,我们应该可以创建出大部分相对简单的LINQ查询.在本篇中,除了对前面的知识做个简单的总结,还会介绍几种创建更复杂查询的方式,让我们在面对更复杂的场景时也能轻松面对,包括:子查询.创建策略和数据转换. 子查询 在创建一个复杂的查询时,通常我们需要用到子查询.相信大家都记得SQL查询里的子查询,在创建LINQ查询时也是如此.在LINQ中,对于方法语法,一个子查询包含在另外一个查询的lambda表达式

LINQ之路 4:LINQ方法语法

书写LINQ查询时又两种语法可供选择:方法语法(Fluent Syntax)和查询语法(Query Expression). LINQ方法语法是非常灵活和重要的,我们在这里将描述使用链接查询运算符的方式来创建复杂的查询,方法语法的本质是通过扩展方法和Lambda表达式来创建查询.C# 3.0对于LINQ表达式还引入了声明式的查询语法,通过查询语法写出的查询比较类似于SQL查询.本篇会对LINQ方法语法进行详细的介绍. 当然,.NET公共语言运行库(CLR)并不具有查询语法的概念.所以,编译器会在

LINQ之路10:LINQ to SQL 和 Entity Framework(下)

在本篇中,我们将接着上一篇“LINQ to SQL 和 Entity Framework(上)”的内容,继续使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术的关键特性.我们在此关注的是LINQ to SQL和Entity Framework中的”LINQ”部分,并会比较这两种技术的相同和不同之处.通过我们之前介绍的LINQ知识还有将来会讨论的更多LINQ Operators,相信阅者能针对LINQ to SQL和Entity Framework写出优雅

LINQ:开始使用 LINQ(五)- LINQ 中的查询语法和方法语法

开始使用 LINQ(五)- LINQ 中的查询语法和方法语法 在表示语言集成查询 (LINQ) 使用 LINQ 性查询语法,文档中的多数查询编写.但是,编译代码时,必须将查询语法转换为方法,这就需要 .NET 公共语言运行时 (CLR).这些方法调用标准查询运算符的名称类似 Where.Select.GroupBy.Join.Max和 Average.可以调用这些方法直接使用方法语法而不是查询语法. 查询语法和方法语法语义相同,但是,许多人员发现查询语法更简单.更易于阅读.某些查询必须表示为方法

LINQ:开始使用 LINQ(一)- 介绍 LINQ 查询

开始使用 LINQ (一)- 介绍 LINQ 查询 查询是一种从数据源检索数据的表达式. 随着时间的推移,人们已经为各种数据源开发了不同的语言:例如,用于关系数据库的 SQL 和用于 XML 的 XQuery. 因此,开发人员不得不针对他们必须支持的每种数据源或数据格式而学习新的查询语言. LINQ 通过提供一种跨数据源和数据格式使用数据的一致模型,简化了这一情况.在 LINQ 查询中,始终会用到对象.可以使用相同的编码模式来查询和转换 XML 文档.SQL 数据库.ADO.NET 数据集..N

LINQ:开始使用 LINQ(三)- 使用 LINQ 进行数据转换

开始使用 LINQ(三)- 使用 LINQ 进行数据转换 语言集成查询 (LINQ) 不仅可用于检索数据,  而且还是一个功能强大的数据转换工具.  通过使用 LINQ 查询,您可以将源序列用作输入,并采用多种方式修改它以创建新的输出序列.您可以通过排序和分组来修改该序列,而不必修改元素本身.但是,LINQ 查询的最强大的功能是能够创建新类型.这一功能在 select 子句中实现. 例如,可以执行下列任务: 一. 将多个输入联接到一个输出序列 1 class Student 2 { 3 publ

Linq之旅:Linq入门详解(Linq to Objects)

示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集成查询).通过LINQ,我们可以使用相同API操作不同的数据源.接下来就让我们看看LINQ是什么以及如何使用? 再此之前,需要先了解的相关技术 1. 隐式类型.匿名类型.对象初始化器 1) 隐式类型,使用var关键字创建,C#编译器会根据用于初始化局部变量的初始值推断出变量的数据类型.(不过我个人认

Linq学习之旅——LINQ查询表达式

1. 概述 2. from子句 3. where子句 4. select子句 5. group子句 6. into子句 7. 排序子句 8. let子句 9. join子句 10. 小结 1. 概述 LINQ的全称是Language Integrated Query,中文译成“语言集成查询”.LINQ作为一种查询技术,首先要解决数据源的封装,大致使用了三大组件来实现这个封装,分别是LINQ to Object.LINQ to ADO.NET.LINQ to XML.它们和.NET语言的关系如下:

Linq学习(一)-初涉Linq

一.何谓LINQ LINQ:Language Integrated Query语言集成查询,其本质是对ADO.NET结果集通过反射连同泛型特性转换成对象集,实现OR模型的转换 二.优点与缺点 优点:封装了SQL语句,只对对象进行操作,代码量减少 缺点:追求效益的同时牺牲了性能,比起ADO.NET性能稍差,且对复制的sql语句也不好操作 三.支持那些查询 联合.分组.排序.连接查询 四.何谓Linq to sql Linq to sql是LINQ的一部分,全称基于关系型数据库的DotNet语言集成