第 109 講:C# 3 之查詢表達式(十三):orderby 的底層原理
今天咱們來講排序的底層。
Part 1 性能優(yōu)化的考慮,不單單是 OrderBy
單個的 orderby
從句里不帶有逗號的情況的話,那么它等價于 OrderBy
,而如果有多個參與排序的依據(jù)的話,會被編譯器翻譯成 ThenBy
。
舉個例子。我想對學生的數(shù)據(jù)進行排序,那么我們可以使用 orderby
來排序:
而這樣的代碼可以翻譯和等價成這樣的調用:
很簡單,對吧。下面我們來說說細節(jié)。
Part 2 OrderBy
和 OrderByDescending
方法
C# 真是不嫌麻煩。只要你給的 orderby
后排序依據(jù)的升序和降序排序規(guī)則關鍵字 ascending
和 descending
不一樣,直接會被編譯器翻譯成兩個方法。是的,OrderBy
對應升序排序 ascending
關鍵字(或者缺省,因為默認就是升序排序),而 OrderByDescending
方法就對應的是 descending
關鍵字的排序。
所以,假設我要降序按 ID 排序的話,那么上面的查詢表達式只需要加一個 descending
關鍵字,對吧。而翻譯成方法調用的話,其實也就是把這里的 OrderBy
方法名字改成 OrderByDescending
。注意,這倆方法名并不相同,只是調用風格和書寫格式差不多,所以感覺上差不多。實際上這是兩個完全不一樣的方法。
如果你把鼠標放在查詢表達式上,你直接可以看到結果:


此文結束。
Part 3 別急,ThenBy
和 ThenByDescending
方法還沒講呢
orderby
從句還沒說完所有的情況呢。
我們知道,orderby
關鍵字是組合的單詞,它并不可拆分。實際上,它的底層對應的是 OrderBy
方法。不過,我們知道 orderby
的用法還可以用逗號分隔多個排序依據(jù)。那么,它是不是等價于多個連續(xù)的 OrderBy
呢?
實際上并不是。雖然這樣我們可以實現(xiàn)出來,但我們如果挨個排序很多項目的時候,我們都使用一次一次的迭代的話,顯然太慢了。于是,C# 語言設計團隊考慮到性能的問題,就制定了一套新的辦法。
排序操作是遍歷完整的序列,因此序列如果比較長的話,我每一次 OrderBy
或 OrderByDescending
方法的調用會導致“反射弧巨長”,太慢。于是,在執(zhí)行 orderby
從句的時候,或者調用實際的 OrderBy
以及 OrderByDescending
方法的時候,實際上編譯器并不會真正去迭代整個序列,而是在整理排序依據(jù)的列表。
這個地方所說的排序依據(jù)的列表,指的是我們在同一個 orderby
從句下,用逗號隔開的眾多排序依據(jù)的整合列表。在多個逗號隔開的時候,它們都不會產(chǎn)生實際上真實的循環(huán)遍歷和排序操作,而是先把排序字段啊、屬性啊,甚至是表達式整個以“延后調用”的方式,緩存起來。思考一個問題哈。對于這種延后調用的操作,比如我排序 100 個學生的 ID,顯然我們的排序依據(jù)是這里的 ID 屬性。而我們不可能用一種合適的語法把“取 ID”的行為給存起來。我們都知道,取值是顯然的執(zhí)行操作,而這個操作是我們無法提前存起來的(操作是不可存儲的)。但是,C# 有一個語法可以做到“存儲取 ID”這樣的操作。是什么呢?對了,委托。
委托就是一種典型的推后、延后或者叫推遲執(zhí)行的好幫手。C/C++ 里有一種這樣類似的概念,叫做函數(shù)指針。函數(shù)指針的目的,就是為了我們把函數(shù)的操作給抽象起來,進行參數(shù)化處理。這樣的話,我們就可以自定義一些執(zhí)行行為,然后在方法的實現(xiàn)上不去考慮,而直接使用這樣的行為來達到靈活代碼的目的。比如我們最開始說的冒泡排序的升降序,我們用到了委托類型的實例作為參數(shù),它實際上就是在方法內使用了這個比較過程,然后直接按這種比較操作來進行排序;而調用方我們則需要給出對應的執(zhí)行操作,比如說 (a, b) => a >= b
。這樣的操作并沒有在我們寫進去的時候就已經(jīng)發(fā)生了,而是在調用方需要用的時候才會自行調用。它的好處就在于,我們把操作提取出來了,而這樣的操作可以靈活運用在真正調用之前的某個時刻,不管是立馬緊接著調用,還是一年之后才會用到,起碼它完成了我們要的目的。
請參照剛才的方法調用的代碼,可以發(fā)現(xiàn) OrderBy
里面也傳入的是一個 Lambda。Lambda 在前面的介紹文字里已經(jīng)說過了,它就是委托類型的一種簡易版實現(xiàn)。所以,它剛好完成了我們的任務。
是的,通過我們反復存儲這些排序依據(jù),就可以完成和達到我們稍后一并排序的處理操作。這就是 C# 對 LINQ 體系里的排序操作(乃至其它 LINQ 操作)的認知。怎么樣,是不是很牛逼?
聽著一頭霧水是吧。因為我還沒說怎么翻譯 ThenBy
之類的東西呢。不過,也不復雜。在 orderby
從句里,第一個逗號左邊的部分(也就是整個 orderby
從句里的第一個排序依據(jù)),會被當成 OrderBy
或者 OrderByDescending
方法;而第一個逗號開始,往后產(chǎn)生的部分都會被翻譯成 ThenBy
和 ThenByDescending
。是的,這兩個方法和 OrderBy
以及 OrderByDescending
也沒有什么特別大的區(qū)別,不過編譯器也確實不嫌麻煩,就搞成四個方法了。搞成四個方法的話,雖然大家都知道用起來肯定是還行的,但是如果我們要自己寫代碼給別人看的話,四個都差不太多的方法不合并成一個方法的話,對方學習使用的時候會很難受。這也是為什么我比較推薦用關鍵字(查詢表達式)來代替使用方法調用的完整寫法的原因。
你想想看,比如說要給各個比賽的隊伍,按照金銀銅獎牌數(shù)排序,那么顯然越多越好,所以金銀銅三個排序依據(jù)都是降序排序。另外,金牌數(shù)量不同的話是不會去看銀牌和銅牌數(shù)的,而是金牌數(shù)多的,排名更靠前;如果金牌一樣多,就看銀牌數(shù);銀牌一樣多再看銅牌數(shù)。如果全部一樣多,我們會認為兩個隊伍是一樣的排名。
對于這種排序過程,寫成查詢表達式實際上很簡單:
而我們用到了連續(xù)的逗號分隔的寫法。這種寫法下,就會被翻譯成這樣:
很好理解對吧。
下面我們來看看最終的對應關系圖。

