這章我們來說說要如何在LINQ中使用排序的功能整理集合,由於LINQ中的排序其實是一組的語法所組合而成的,所以今天會講到多個不同的語法,雖然說是多個語法,但是關鍵都還是圍繞在排序這個目的上,只是使用的情境會不同而已,讓我們來看看這些語法各有什麼樣的作用吧。
OrderBy設定第一個排序條件,而且此排序條件為遞增排序。
OrderByDescending設定第一個排序條件,而且此排序條件為遞減排序。
ThenBy設定第二個以後的排序條件,此排序條件為遞增排序。
ThenByDescending設定第二個以後的排序條件,此排序條件為遞減排序。
本章會講解以上四個方法,藉由說明我們可以看到它們其實只有順序及遞增遞減的差別而已,性質其實是相同的。
這組方法主要目的就是排序資料,請看下面的例子(節錄自Microsoft Docs):

char[] Source = new char[] { 'G', 'C', 'F', 'E', 'B', 'A', 'D' };
IOrderedEnumerable<char> Results = Source.OrderBy(c => c);
foreach(char result in Results){
    Console.Write($"{result} ");
}
Console.WriteLine();
// output: A B C D E F G
由上面的例子我們知道了OrdeBy的作用是將資料遞增排序,讓我們來看看它的定義:
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);
keySelector: 要排序的欄位comparer: 客製的比較器IOrderedEnumerable: LINQ排序語法都會回傳此型別OrderBy有兩個方法,差別在於要不要傳入客製的比較器,客製的比較器是使用在你有特別的排序方式的情況時,等下的範例程式我們做個範例。
這裡我們會看到一個特別的回傳型別IOrderedEnumerable,它繼承自IEnumerable,會定義這個型別的目的是為了要讓ThenBy及ThenByDescending可以接續在這個排序之後再做其他的排序。
當然,因為IOrderedEnumerable是繼承自IEnumerable,所以你要在後面用OrderBy也是合法的,但是這樣做會讓它忽略之前的排序,將其本身視為第一個排序條件,我們之後的範例程式再來看看。
接下來的OrderByDescending的方法定義:
public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector);
public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    IComparer<TKey> comparer);
整個方法結構跟OrderBy完全一樣,只是差在它的結果會是遞減的。
最後我們來看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)
public static IOrderedEnumerable<TSource> ThenByDescending<TSource, TKey>(
    this IOrderedEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector)
public static IOrderedEnumerable<TSource> ThenByDescending<TSource, TKey>(
    this IOrderedEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    IComparer<TKey> comparer)
前面有說到ThenBy及ThenByDescending是要接在其他排序的後面,現在看到定義this的確是需要傳入IOrderedEnumerable,所以代表你在一個IEnumerable的後面是不能接ThenBy的,要先接OrderBy才能用ThenBy。
依據C# Spec可以照到下面跟orderby相關的定義:
orderby_clause
    : 'orderby' orderings
    ;
orderings
    : ordering (',' ordering)*
    ;
ordering
    : expression ordering_direction?
    ;
ordering_direction
    : 'ascending'
    | 'descending'
    ;
orderby查詢運算式後面可以接多個排序條件(以,分隔)ascending或descending來決定要遞增還是遞減來看一個查詢運算式的例子:
string[] words = new string[] { "Apple", "Banana", "Cherry", "Donut", "Eat", "Football" };
IOrderedEnumerable<string> results = from word in words
                                     orderby word.Substring(1 1),
                                             word.Substring(2,1) descending
                                     select word;
...
可以看到查詢運算式的排序是非常直覺的,相較於方法要用各個不同的方法來排序,運算式的語法更像是我們熟悉的SQL。
我們可以依照C# Spec的定義將下面的運算式:
from x in e
orderby k1 , k2 , ..., kn
...
轉成下面的方法:
from x in ( e ) . 
OrderBy ( x => k1 ) . 
ThenBy ( x => k2 ) .
... .
ThenBy ( x => kn )
...
可以看到查詢運算式所轉出來的方法是按照OrderBy.ThenBy的方式實作。
想要將偶數排在奇數之前,我們可以實作一個客製的比較器如下:
int[] numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
IOrderedEnumerable<int> results = numbers.OrderBy(x => x, newCustomComparer());
...
class CustomComparer : IComparer<int>
{
    public int Compare(int x, int y)
    {
        return x % 2 - y % 2;
    }
}
// output: 0 2 4 6 8 1 3 5 7 9
OrderBy是Stable Sort所以會依照原本的序列排序OrderBystring[] words = new string[] { "Apple", "Banana", "Cherry", "Donut", "Eat", "Football" };
IOrderedEnumerable<string> results = words
    .OrderBy(x =>x.Substring(1, 1))
    .OrderByDescending(x => x.Substring(2, 1));
/* output: 
 * 
 * Eat
 * Apple
 * Football
 * Banana
 * Donut
 * Cherry
 */
IOrderedEnumerable<string> results2 = words
    .OrderBy(x =>x.Substring(1, 1))
    .ThenByDescending(x => x.Substring(2, 1));
/* output: 
 * 
 * Eat
 * Banana
 * Cherry
 * Football
 * Donut
 * Apple
 */
由結果可以看出不用ThenBy的話第一個查詢條件形同虛設,根本沒有用到。
GetEnumerator()或是foreach觸發才會做巡覽IEnumerable而是IOrderedEnumerable,目的是要讓ThenBy及ThenByDescending接續其後設定其他的排序條件IComparer來客製比較器這一章我們學了4個排序方法,OrderBy家族會排在第一個條件,而ThenBy家族則是排在第二到第n個條件,利用Descending搭配讓我們可以遞增遞減排序。