第 100 講:C# 3 之查詢表達式(四):orderby 關(guān)鍵字
orderby
從句。這個詞也是一個兩個單詞構(gòu)成的組合關(guān)鍵字,和 readonly
、foreach
這些關(guān)鍵字是類似的。
Part 1 引例:按指定屬性執(zhí)行排序操作
我們?nèi)匀皇褂弥敖榻B的例子來說明。我們在前面學習了如何定義臨時變量、如何篩選對象、并且如何使用映射機制。今天我們來說排序。
考慮一種情況。我還是那些同學,我想按照名字的順序來排序。名字因為剛好都寫成英語,所以我們干脆就按照英語單詞的字典序比較方式來對這些學生進行排序,并得到排序后的結(jié)果。前面的查詢表達式是沒辦法排序的,今天我們來學習一下排序的做法。
排序我們使用 orderby
關(guān)鍵字來表示:
請注意這個寫法。今天這個寫法有一點點理解難度。第 3 行 orderby student.Name ascending
表示將 student.Name
取出來,然后排序。每一個學生都會如此,因此我們只需要寫 student.Name
就可以表達“我想要按照每一個學生的 Name
屬性排序”的意思。接著,在 student.Name
后有一個全新的關(guān)鍵字:ascending
。這個詞比較生疏,它在編程里的意思是“升序的”,換句話說,就是按照從小到大的順序表達出來的過程。對于字符串而言,從小到大的概念就等價于字典序。字母表里越靠前的字母越小,越靠后的越大。因此,a 字母最小、z 字母最大。這剛好復合我們需要的排序手段;如果你要降序排序的話,用的是 descending
這個關(guān)鍵字來代替 ascending
。
接著,orderby
從句也不能作為結(jié)尾,因此它后面還是得繼續(xù)寫一句 select student
。請注意這個時候的 student
變量。student
在整個查詢表達式都是同一個變量,但是實際上在第 3 行的時候,我們將其已經(jīng)排序完成了。最后的 select
從句里就相當于要把它給拿出來返回,因此我們直接寫 select student
即可。如果你要排序之后只獲取所有學生的名字的話,這里就改成 select student.Name
即可。
可以發(fā)現(xiàn),這樣的排序操作未免過于簡單了一些。直接寫出來甚至都不用考慮底層。這就是 orderby
關(guān)鍵字的用法。另外需要注意的是,如果 orderby
從句如果是升序排序的話,是可以省略不寫 ascending
關(guān)鍵字的;但如果要降序排序的話,就必須要寫上 ascending
關(guān)鍵字了。比如前面的代碼用的是升序,因此可以不寫 ascending
。
這樣就可以了。
Part 2 遞進排序
假設(shè)我們把前面的代碼稍微改一下。假設(shè)我要排序?qū)W生的名字,如果學生名字一樣的話,我們就繼續(xù)按學生的年齡升序排序。這樣的代碼需要遵循兩個排序規(guī)則,不過它們肯定不是同時發(fā)生的,而是前者在得到一致結(jié)果之后,再按照后者的內(nèi)容排序。這樣的語句我們可以使用逗號分隔,寫在一起:
orderby
關(guān)鍵字只需要寫一次,但是逗號要分開每一個排序的過程。每一個過程都帶有排序的屬性名稱,以及排序的升序或降序的關(guān)鍵字。和前面一樣,如果是 ascending
Part 3 不建議出現(xiàn)多個 orderby
從句
orderby
從句和 let
、where
類似,都是寫在 from
和 select
期間的,它不能當開頭也不能當結(jié)尾。但是,可以多次出現(xiàn) orderby
從句。比如下面的代碼:
Name
屬性,然后篩選出來所有成年的學生,然后再次對篩選結(jié)果進行排序,按 Age
屬性排序。最后取出學生對象即可。
可以看到,這個例子里用到了兩次 orderby
從句,不過它們是互不影響的。按照道理來說,先按照 Name
屬性排序后,序列變?yōu)檫@樣,然后再次按 Age
重新排序,那么原來的序列的順序就再一次會被打亂。不過這個例子里,中間有一個 where
從句,因此會去掉一些不滿足條件的對象,隨后再次排序也僅僅針對于成年的學生,然后才進行排序的。
不過,按照邏輯合理性來說的話,其實多次 orderby
并不是非常好的寫法。如果真的這樣的語句是合理的的話,那么我刪掉前面的 orderby
語句好像也沒問題:
我只保留 orderby s.Age
好像也沒問題。所以,我們不太建議多次排序同一個對象,還按照不同的排序形式進行排序,這樣的話,本來排序后的序列會被再一次打亂,那么前一次的排序操作其實并沒有任何用處。
Part 4 排序依據(jù)跟對象無關(guān)的情況
4-1 排序依據(jù)是通過對象本身的數(shù)據(jù)計算得到的
考慮一下。我們需要按多個屬性進行混合使用,然后排序的話,我們要怎么做呢?
最容易想到的場景,就是平均數(shù)。假設(shè)我要把學生按平均數(shù)進行降序排序的話,我們的代碼是這樣的:
請觀察這個排序操作。orderby
從句的排序依據(jù)雖然是平均成績,跟學生還是有關(guān)系,但實際上在書寫代碼的時候,我們已經(jīng)單獨創(chuàng)建了新的臨時變量,因此排序的結(jié)果實際上跟 s
關(guān)系已經(jīng)不大了。
當然,你可以不使用 let
從句,而改用表達式的形式書寫:
C# 是允許表達式書寫進來的。
4-2 排序依據(jù)是一個跟序列完全無關(guān)的常量
排序行為不一定非得是來自于對象本身的屬性或字段取值,它可以是表達式,甚至是一個常量。我們來看一個比較奇怪的例子。
假設(shè)我要給這個數(shù)組排個序。結(jié)果,我在 orderby
里寫的是一個常量 0。你知道這個排序機制意味著什么嗎?排序的依據(jù)已經(jīng)跟數(shù)組的每一個元素都沒有任何關(guān)系了。前面的例子好歹還有關(guān)系,這個壓根沒有任何關(guān)系了。
那么你知道結(jié)果如何嗎?其實很簡單。因為排序依據(jù)是常量,也就意味著每一次排序和比較操作都是這個數(shù)值作為判斷的依據(jù)和標準。因此,排序的東西是什么已經(jīng)無所謂了,這樣的數(shù)據(jù)傳入進來,怎么進來的就怎么出去。因此使用常量作為排序依據(jù)的查詢表達式,執(zhí)行結(jié)果和原始數(shù)據(jù)的序列順序相比,沒有任何變動。
這個不合常理的寫法可以推廣到任何常量上。因為常量是不變的,因此所有的常量當作排序依據(jù)最終的結(jié)果都不會發(fā)生任何變動。哪怕你寫一個字符串、寫一個整數(shù)、寫一個浮點數(shù),甚至寫多個都是常量的排序依據(jù),全部沒有任何關(guān)系。
你以為寫得多就有變化?笑話。因為排序依據(jù)全部是常量,所以你寫再多都不會變。因此,我們強烈不建議將常量作為排序依據(jù)寫進 orderby
從句里,雖然 C# 查詢表達式允許你這么做,它畢竟不是語法錯誤。
Part 5 其它細節(jié)
5-1 不要往 orderby
從句里寫不能排序的對象和表達式
排序好用是好用,但是也有一點比較隱蔽的問題。orderby
里是可以寫任何東西的,因此有些并不支持排序的對象也可以放進去。比如我們之前設(shè)計的 Student
類型,它并不包含比較的任何操作,如果我們直接這么寫的話:
C
C
然后就直接開始排序:
你覺得這樣的代碼它合理嗎?顯然不合理。對象 c
是 C
類型的,但這個類型都壓根不能比較。我們知道,要想對象參與排序操作,必須對象至少得支持比較操作。但很明顯,這個 C
類型,還有前面的 Student
類型并沒有實現(xiàn) IComparable
和 IComparable<>
接口。繼續(xù)的排序操作都沒有,怎么可能可以呢?
但是,C# 考慮到一些處理機制的靈活性,它允許我們這么寫代碼。說實話,它確實并不算是一個語法錯誤。但是,運行期間,如果程序執(zhí)行代碼期間沒有發(fā)現(xiàn)你這個排序的對象對應(yīng)的類型里包含實現(xiàn)了 IComparable
或 IComparable<>
接口的話,就會產(chǎn)生 InvalidOperationException
類型的異常,告訴你對象不能比較大小,因此排序失敗。
5-2 如果 where
和 orderby
挨著,一般先 where
后 orderby
考慮前面給的代碼:
如果我寫成這樣:
好像排序的結(jié)果是一樣的,先后順序好像顯得不是那么重要。不過性能上是有區(qū)別的。前者先使用 where
的話,或多或少會刪掉一些不滿足條件的元素,因此排序的元素數(shù)量可能沒有原始整個集合那么多;但是后者就不會這樣了。因為后者先寫的是 orderby
,因此它會優(yōu)先執(zhí)行一次對整個集合的排序操作,然后排序完成后才開始篩選序列。顯然排序的速度會慢一些,因此我們強烈不建議使用后者這樣的模式去書寫代碼,即我們不建議在 where
和 orderby
從句緊挨著出現(xiàn)的時候,先寫 orderby
后寫 where
。
5-3 排序的底層怎么排序的呢?用的什么排序方式呢?
呃,這個確實是超綱了,因為現(xiàn)在僅憑單純的關(guān)鍵字確實不太好講清楚。我只能告訴你答案了:底層用的是快速排序法。快速排序是不穩(wěn)定的排序,如果序列的數(shù)值比較極端,可能會導致交換次數(shù)超出預期的情況,排序效果不好;但多數(shù)時候會非??焖俚玫浇Y(jié)果,正是因為如此,這個算法才會叫快速排序,因為它確實很快。
如果你要看代碼的話,實際上三兩句是講不清楚的,雖然你可以大體看出執(zhí)行的邏輯是快速排序,但具體怎么按具體的排序依據(jù)進行排序的細節(jié)我們?nèi)耘f沒辦法說清楚,所以自己看代碼吧:
https://source.dot.net/#System.Linq/System/Linq/OrderedEnumerable.cs,a25a731c74bcfb10,references
你放心,它們的底層我們肯定會說的,只是現(xiàn)在因為先說的話難度過大,所以就暫時賣個關(guān)子。在后面講解“任意類型的查詢表達式”的深層次 LINQ 機制的時候,會給大家講解原理。
5-4 orderby
從句的結(jié)果是 IOrderedEnumerable<>
接口實例
orderby
IOrderedEnumerable<>
接口類型的實例,而這個接口是一個全新的類型。不過,這個接口類型仍然實現(xiàn)了 IEnumerable<>
接口,因此這種接口類型的實例仍然支持 foreach
循環(huán)去迭代它。所以用法和使用體驗上幾乎和一般的 IEnumerable<>
接口沒有任何的區(qū)別。不過因為它的底層有些麻煩,所以現(xiàn)在還說不清楚這樣設(shè)計的好處,所以咱先當成結(jié)論記住就行。之后講解原理的時候我們會闡述細節(jié)。
Part 6 總結(jié)
至此我們就對 orderby
關(guān)鍵字有了一個比較完善的認知。包括單排序、多排序的語法、多次使用 orderby
從句的性能問題、orderby
和 where
先后順序的性能問題以及不建議使用跟集合序列無關(guān)的量作為排序依據(jù)的相關(guān)內(nèi)容。
從下一節(jié)的內(nèi)容開始,就比較難一些了。最難理解的莫屬 group-by
系列從句和 join
系列從句了,我們也只剩下了這倆沒有講了。當然了,說完了查詢表達式的基本語法和關(guān)鍵字,也只說完了整個 LINQ 體系的三分之一,后面還有驚險刺激的內(nèi)容等著你去學習。加油吧騷年!