iT邦幫忙

DAY 22
5

分享一些學習心得系列 第 22

LINQ自學筆記-語法應用-資料排序-OrderBy、ThenBy 和遞減

本文將為大家介紹 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)欄位做遞增排序,最後輸出結果。這邊用的是第一個多載方法,也是最簡單的方式。

接下來我們用幾個問題來帶出後續內容:

  1. 可以遞減排序嗎?
  2. 可以指定多個排序欄位嗎?
  3. 可以同時使用多個欄位進行遞增和遞減排序嗎?
  4. 可以自訂排序邏輯嗎?

第一個問題,遞減排序很簡單,改用 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

上一篇
LINQ自學筆記-語法應用-型別篩選-OfType 運算子
下一篇
LINQ自學筆記-語法應用-單項資料-First、Last、ElementAt、Single
系列文
分享一些學習心得30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言