1. 程式人生 > >LINQ之路14:LINQ Operators之排序和分組(Ordering and Grouping)

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

turn clas 特殊 line source 一行 匿名 生成 應用

本篇繼續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)。

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