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