本文將為大家介紹 LINQ 排序的四個運算子:OrderBy、OrderByDescending、ThenBy、ThenByDescending,以及如何自訂排序邏輯。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
OrderBy 運算子可以讓我們用「遞增」的方式排序資料。以下為 OrderBy 兩個多載方法:
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector
)
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IComparer<TKey> comparer
)
使用 OrderBy 方法,我們必須告訴它要排序的欄位,也就是 keySelector 這個委派方法(傳入參數),我們可以用查詢表達式和方法架構查詢語法來撰寫:
void Main()
{
//建立訂單資料來源
var orders = new List<Order>(){
new Order {CustomerId = 3, OrderDate = new DateTime(2011, 10, 9), Total = 2940},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 10, 10), Total = 3849},
new Order {CustomerId = 1, OrderDate = new DateTime(2011, 12, 1), Total = 500},
new Order {CustomerId = 1, OrderDate = new DateTime(2012, 2, 28), Total = 1234},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 5, 20), Total = 9520}
};
//依 Total 做遞增排序
var queryOrder = from e in orders
orderby e.Total
select e;
//對等的方法架構查詢語法
//var queryOrder = orders.OrderBy (e => e.Total);
foreach (var e in queryOrder)
{
Console.WriteLine(e.ToString());
}
}
//訂單基本資料類別
public class Order
{
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public double Total { get; set; }
public override string ToString()
{
return string.Format("CustomerId = {0}, OrderDate = {1}, Total = {2}",
CustomerId, OrderDate, Total);
}
}
/* 輸出:
CustomerId = 1, OrderDate = 2011/12/1 上午 12:00:00, Total = 500
CustomerId = 1, OrderDate = 2012/2/28 上午 12:00:00, Total = 1234
CustomerId = 3, OrderDate = 2011/10/9 上午 12:00:00, Total = 2940
CustomerId = 2, OrderDate = 2012/10/10 上午 12:00:00, Total = 3849
CustomerId = 2, OrderDate = 2012/5/20 上午 12:00:00, Total = 9520
*/
上述程式碼,我們建立一個 List<Order> 的資料來源清單,然後用 LINQ 查詢,設定用訂單金額(Total)欄位做遞增排序,最後輸出結果。這邊用的是第一個多載方法,也是最簡單的方式。
接下來我們用幾個問題來帶出後續內容:
第一個問題,遞減排序很簡單,改用 OrderByDescending 運算子即可。注意,在查詢表達式時,請在排序欄位名稱後加上 descending 關鍵字:
//查詢表達式
var queryOrder = from e in orders
orderby e.Total descending
select e;
//對等的方法架構查詢
var queryOrder = orders.OrderByDescending(e => e.Total);
OrderByDescending 運算子的應用方法和 OrderBy 運算子完全相同,不再贅述。
第二個問題,是,我們可以指定多個排序欄位,但要注意在方法架構查詢時,要用 ThenBy 運算子串接後續欄位,每個欄位都要用一個 ThenBy 運算子:
//查詢表達式
var queryOrder = from e in orders
orderby e.CustomerId, e.OrderDate
select e;
//對等的方法架構查詢
var queryOrder = orders.OrderBy (e => e.CustomerId).ThenBy(e => e.OrderDate)
ThenBy 運算子比較特別,以下的它的多載方法:
public static IOrderedEnumerable<TSource> ThenBy<TSource, TKey>(
this IOrderedEnumerable<TSource> source,
Func<TSource, TKey> keySelector
)
public static IOrderedEnumerable<TSource> ThenBy<TSource, TKey>(
this IOrderedEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IComparer<TKey> comparer
)
注意到了嗎,ThenBy 運算子是擴充 IOrderedEnumerable<TSource>,而它正是 OrderBy、OrderByDescending 運算子的回傳型別,所以 ThenBy 一定要用在 OrderBy、OrderByDescending 之後。另外 ThenBy 運算子本身也是回傳 IOrderedEnumerable<TSource>,所以我們可以串接多個 ThenBy 運算子,借以滿足多個欄位排序之需求。
第三個問題,可以指定多個排序欄位且混用遞增和遞減邏輯嗎?答案是可以,利用前面講的 OrderBy、OrderByDescending、ThenBy 以及 ThenByDescending 運算子即可:
//查詢表達式
var queryOrder = from e in orders
orderby e.CustomerId, e.OrderDate descending
select e;
//對等的方法架構查詢
var queryOrder = orders.OrderBy(e => e.CustomerId).ThenByDescending (e => e.OrderDate)
上述範例,先用顧客編號(CustomerId)做遞增排序,再依訂單日期(OrderDate)做遞減排序。這是常見的查詢排序方法。
第四個問題,可以自訂排序邏輯嗎?答案也是可以。
若使用 OrderBy 第一個多載方法,會以要排序欄位值的型別之「預設」排序方法,做遞增排序,如果希望使用非預設方法排序,則我們可以使用第二個多載方法,傳入一個實做 IComparer<TKey> 的型別,自訂排序規則:
void Main()
{
string[] its = {"KK", "RR", "FF", "TT", "CC", "JJ"};
JJAlwaysOnTop comparer = new JJAlwaysOnTop();
var query = its.OrderBy (i => i);
query.Dump("預設排序結果");
query = its.OrderBy(i => i, comparer);
query.Dump("自訂排新結果");
}
public class JJAlwaysOnTop:IComparer<string>{
public int Compare(string s1, string s2)
{
if (s1.Equals(s2)) return 0;
if ("JJ".Equals(s1)) return -1;
else if ("JJ".Equals(s2)) return 1;
return s1.CompareTo(s2);
}
}
上述程式碼,我們定義了一個字串陣列,先輸出依欄位值預設之排序演算法的結果,然後再建立一個自訂的排序類別,裡面強迫「JJ」這個字串,在排序時,永遠是最小,以達成在遞增排序時,永遠排在第一位的結果。
最後,免費再送一個問題:可以不要用 ThenBy,直接用多個 OrderBy 運算子嗎?
答案是,編譯和執行都不會發生錯誤,但是輸出結果和預期可能完全不同喔:
var queryOrder = orders.OrderBy(e => e.Total).OrderBy(e => e.CustomerId);
可以預期上述輸出結果為何嗎?先依總金額排序,再用顧客編號排序嗎?No! No! 正確答案是會忽略第一個 OrderBy 方法,直接使用第二個 OrderBy 方法所指定的排序欄位,也就是用 CustomerId 做遞增排序:
CustomerId = 1, OrderDate = 2011/12/1 上午 12:00:00, Total = 500
CustomerId = 1, OrderDate = 2012/2/28 上午 12:00:00, Total = 1234
CustomerId = 2, OrderDate = 2012/10/10 上午 12:00:00, Total = 3849
CustomerId = 2, OrderDate = 2012/5/20 上午 12:00:00, Total = 9520
CustomerId = 3, OrderDate = 2011/10/9 上午 12:00:00, Total = 2940