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)。
LINQ之路14:LINQ Operators之排序和分組(Ordering and Grouping)