天天看點

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參數

IOrderedEnumerable <TSource>

查詢表達式文法

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

GroupBy 對一個sequence進行分組 GROUP BY

元素選擇器/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)。

作者:Cat Qi

出處:http://qixuejia.cnblogs.com/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。