最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

第 101 講:C# 3 之查詢表達(dá)式(五):group 和 by 關(guān)鍵字

2022-04-10 10:33 作者:SunnieShine  | 我要投稿

今天我們來(lái)繼續(xù)查詢表達(dá)式。今天我們講解的從句是 group-by 從句??梢钥吹?,這個(gè)從句的 group-by 寫法是分開(kāi)的,這里就和 orderby 從句不太一樣了。至于這么取名的原因我們之后再說(shuō),我們先說(shuō)一下這個(gè)從句的基本用法。

Part 1 引例:按性別分組

考慮一種情況。假設(shè)我要把整個(gè)序列按照男女分成兩組的話,應(yīng)該怎么做呢?

C# 提供了一個(gè)新的從句:group-by 從句,我們需要這么寫代碼:

是的,看起來(lái)未免有點(diǎn)太簡(jiǎn)單了。我們引入的 group-by 從句寫法是這樣的:

也就是說(shuō),我們需要在 groupby 關(guān)鍵字的中間插入一個(gè)表達(dá)式,表示我要將整個(gè)表達(dá)式按什么結(jié)果作為分組的基本信息,而 by 后面,則是分組的依據(jù)。這個(gè) groupby 中間的表達(dá)式,理解起來(lái)有些類似 select 從句后面跟的那個(gè)表達(dá)式。它可以是原始的迭代變量本身,也可以是一個(gè)其它的表達(dá)式,它寫出來(lái)是作為映射的。這里的也是一樣,我們要想將學(xué)生按男女分組,需要達(dá)到的條件有兩點(diǎn):

  1. 分組依據(jù)是學(xué)生的性別(即 Gender 屬性);

  2. 分組后,我們最后得到的每一個(gè)元素仍然得是學(xué)生類型的實(shí)例,而不是單單取它的名字什么的。

因?yàn)槲覀円_(dá)到這樣兩點(diǎn)的條件,因此這里我們寫的是 group student by student.Gender。

可以看到,此時(shí) group-by 從句的特殊之處就出來(lái)了:它是繼 select 從句之后第二個(gè)可以放在結(jié)尾的從句類型。實(shí)際上,和 select 一樣,它也只能放在末尾;如果要放在中間,也得和 select 從句一樣,末尾加一個(gè) into 從句部分來(lái)接續(xù)。不過(guò)這個(gè)就扯遠(yuǎn)了,我們一會(huì)兒再說(shuō)。先來(lái)說(shuō)表達(dá)式的結(jié)果。

Part 2 分組查詢表達(dá)式結(jié)果的使用

篩選出來(lái)之后是什么樣的結(jié)果呢?使用怎么使用呢?下面我們就來(lái)說(shuō)一下如何去使用這個(gè)表達(dá)式的結(jié)果。

2-1 IGrouping<TKey, TElement> 接口類型

先來(lái)說(shuō)一個(gè)全新的接口類型:IGrouping<TKey, TElement>。這個(gè)接口是不是很像是 Dictionary<TKey, TValue>?是的,這個(gè)接口算是 LINQ 機(jī)制里的字典類型里對(duì)一個(gè)鍵值對(duì)的抽象。換句話說(shuō),這個(gè)接口類型差不多在理解上就是“一個(gè)鍵值對(duì)類型”。這個(gè) IGrouping<TKey, TElement> 接口類型好比是你實(shí)例化字典期間,初始化傳入進(jìn)去的鍵值對(duì)。只不過(guò)這個(gè)接口類型是“一個(gè)鍵對(duì)應(yīng)一組值”,而字典的鍵值對(duì)則是“一個(gè)鍵對(duì)應(yīng)一個(gè)值”。所以嚴(yán)謹(jǐn)一點(diǎn)的話呢,IGrouping<TKey, TElement>Dictionary<TKey, IReadOnlyCollection<TElement>> 的其中一個(gè)鍵值對(duì)的意思差不多。

好像有點(diǎn)繞。我們舉個(gè)例子吧。使用一下上面查詢表達(dá)式得到的結(jié)果。

2-2 使用 IGrouping<,> 接口和 Key 屬性

我們寫兩層 foreach 循環(huán)。外層 foreach 循環(huán)用來(lái)遍歷所有的分組。這每一個(gè)分組都對(duì)應(yīng)了一種情況,也就是前面我們要求的按性別分組的機(jī)制。比如例子要求按性別分組,那么整個(gè)查詢表達(dá)式的結(jié)果就是由兩個(gè)分組構(gòu)成的集合;而這兩個(gè)分組的區(qū)別就是性別不一樣。同一個(gè)分組里的所有元素,性別都一樣,但兩個(gè)分組是按性別分的,所以所有男生在一組,所有女生在一組。

接著,我們使用 IGrouping<,> 接口里面自帶的 Key 屬性,獲取分組依據(jù)。這個(gè)分組依據(jù)指的就是性別(即 Gender 屬性的數(shù)值)。因?yàn)椴僮魇前茨信M(jìn)行分組,所以 Key 屬性就對(duì)應(yīng)男或者女,具體是哪一個(gè),就看這個(gè)分組是男生組還是女生組。

最后,我們?cè)诶锩嬖賹懸粋€(gè) foreach 循環(huán),表示迭代和遍歷整個(gè)這一組里的所有元素。迭代方法就是把這個(gè) IGrouping<,> 類型的實(shí)例給拿出來(lái),然后直接 foreach 它即可。

因此,整個(gè)上述的描述可以表達(dá)成下面這樣的代碼。

我們來(lái)看一下運(yùn)行效果:

可以看到,Female 是一組,Male 則是另外一組。每一組的元素在輸出顯示的時(shí)候,我都在模式字符串上加上了四個(gè)空格,用來(lái)表示類似縮進(jìn)的概念。這樣看到的結(jié)果就可以更容易區(qū)分 Female 的元素有哪些,而 Male 的元素有哪些。

這就是分組的用法。通過(guò)分組查詢表達(dá)式,我們可以得到的結(jié)果是一個(gè)若干結(jié)果構(gòu)成的集合,這若干結(jié)果是按照指定的東西分組之后的結(jié)果;而每一個(gè)結(jié)果,都代表了一組元素,它們的分組依據(jù)是相同的。因此,從這個(gè)漢字表達(dá)可以推導(dǎo)出,group-by 從句在執(zhí)行后,表達(dá)式反饋的結(jié)果類型應(yīng)該是 IEnumerable<IGrouping<Gender, Student>> 類型的。是的,確實(shí)有些長(zhǎng)了。不過(guò)我們可以拆解開(kāi)去理解它:

如果這么去看的話,應(yīng)該就不難理解了;而 IEnumerable<> 類型本身就可以 foreach,而 IGrouping<,> 也是一個(gè)“一對(duì)多”的鍵值對(duì),它的 foreach 就等于是在遍歷里面的每一個(gè)值。所以這就是為什么,這個(gè)分組查詢表達(dá)式的結(jié)果需要用兩層 foreach 來(lái)搞定。

Part 3 可分組的對(duì)象類型

既然都說(shuō)了分組了,自然我們就要說(shuō)說(shuō),什么樣的對(duì)象可以使用 group-by 從句。

group-by 從句使用 groupby 兩部分構(gòu)成,group 后會(huì)跟一個(gè)變量或者表達(dá)式,而 by 后則也會(huì)跟一個(gè)變量或表達(dá)式。其中起到分組效果的是 by 這部分而不是 group。實(shí)際上 group 這部分的表達(dá)式只是表示分組后,將什么結(jié)果作為反饋,因此阿貓阿狗都可以;而 by 后必須要是可以進(jìn)行分組的表達(dá)式。

那么,啥樣的東西可以分組呢?有相等性比較功能的對(duì)象類型唄。顯然,我隨便使用一個(gè)啥都沒(méi)有的實(shí)例去 group-by 就肯定不行:

我這有一個(gè)空的 C 類型,啥代碼都不實(shí)現(xiàn)。然后我們?cè)囍鴮?duì) C 來(lái)分組:

注意 by 后直接寫的是 c。那么這樣的代碼可行嗎?顯然不行。c 啥都沒(méi)實(shí)現(xiàn),怎么可能可以成功分組呢?要想正確分組,只要你得比較判斷這個(gè)對(duì)象是不是這一組的數(shù)據(jù)。那么,比較自然指的就是相等性的比較了。難不成你分個(gè)組還要大小比較一下?肯定不是嘛。

如果你實(shí)在理解不了,你可以想一下,假設(shè)你是圖書館的管理員,作為管理員你需要分門別類地放置書籍。你的做法肯定是拿起一本書,然后看一下書本自己的分類,然后去拿這個(gè)去和書架上印的分組名進(jìn)行比對(duì)。比對(duì)發(fā)現(xiàn)是一樣的,我們才會(huì)放進(jìn)去,對(duì)吧。那么這個(gè)比對(duì)過(guò)程用編程的視角來(lái)說(shuō),就是字符串的逐個(gè)字符比較。而且這個(gè)“比對(duì)一樣”指的就是字符串相等。

因此,上面這樣的 group c by c 的寫法就是不合適的。要想允許一個(gè)對(duì)象(或者這個(gè)類型的表達(dá)式)能夠參與分組操作(即寫在 group-by 從句的 by 后面),至少要求對(duì)象的數(shù)據(jù)類型可以進(jìn)行相等性比較。嚴(yán)謹(jǐn)一點(diǎn)的話呢,就是必須至少得重寫掉 object 派生下來(lái)的這個(gè) Equals 方法,或者是實(shí)現(xiàn) IEquatable<> 接口。當(dāng)然,我們推薦實(shí)現(xiàn) IEquatable<> 這個(gè)接口來(lái)達(dá)到相等性比較的功能,這樣的話性能會(huì)稍微好一些,比起重寫 Equals 方法來(lái)說(shuō)。因?yàn)?Equals 方法的參數(shù)是 object 類型的,而不是具體的數(shù)據(jù)類型。

Part 4 group-by-into 從句

如果單純只是看分組的話,其實(shí)理解起來(lái)難度還行。如果要分組之后繼續(xù)使用結(jié)果的話,就不太容易去理解了。

4-1 引例

考慮一個(gè)情況。我在構(gòu)造器里傳入了四個(gè)參數(shù),如果第二個(gè)參數(shù)代表的是學(xué)號(hào)的話,并且我們把學(xué)號(hào)的前四位當(dāng)成入學(xué)年份來(lái)看,那么如果我們想要按這些學(xué)生的入學(xué)年份來(lái)進(jìn)行分組的話,我們可以這么做:

是的,student.Id.Substring(0, 4) 可以得到每一位學(xué)生的學(xué)號(hào)(假設(shè)用 Id 屬性來(lái)表示的話),然后取出整個(gè)字符串的前四個(gè)字符。

運(yùn)行結(jié)果也是合理的:

唯一的一組是,我們只是分組,所以并不能保證分組后的序列是真正按照年份進(jìn)行升序或降序排序的。那么我們就需要對(duì)結(jié)果進(jìn)行排序。怎么做呢?

我們接續(xù) group-by 從句,在后面加上 into,并給分組結(jié)果取名叫 currentGroup;接著,我們?cè)诤竺媸褂?orderby 從句對(duì)齊進(jìn)行升序排序;最后別忘了 select 從句(因?yàn)?orderby 是不能作結(jié)尾的)。所以代碼是這樣的:

稍微啰嗦一點(diǎn)說(shuō)一下代碼。這里我們插入了兩個(gè) let 從句,是因?yàn)槲覀円茨攴萆蚺判?,但?wèn)題是字符串的大小比較我們比較生疏。字符串是可以進(jìn)行大小比較的,看的是字典序。不過(guò)我們要按照年份排序,更為嚴(yán)謹(jǐn)正確的處理過(guò)程是,將字符串表達(dá)的整數(shù)數(shù)值給解析為真正的整數(shù),然后進(jìn)行大小比較。

然后,into 從句后跟的 currentGroup 是什么類型的呢?沒(méi)錯(cuò),和 select-into 從句類似,select-into 從句的 into 后跟的變量是 select 從句給的當(dāng)前表達(dá)式的運(yùn)算結(jié)果。正因?yàn)槿绱耍?/span>group-by-intointo 后也是當(dāng)前 group-by 操作得到的當(dāng)前結(jié)果。所以,它是什么類型的呢?沒(méi)錯(cuò),IGrouping<string, Student> 類型的!第一個(gè)泛型參數(shù)的實(shí)際類型 string 對(duì)應(yīng)分組依據(jù)是入學(xué)年份(的字符串表達(dá),因?yàn)槭?Substring 處理后的結(jié)果類型),而第二個(gè)泛型參數(shù)的實(shí)際類型 Student 就是每一個(gè)當(dāng)前入學(xué)年份的學(xué)生的實(shí)例。因?yàn)樗?IGrouping<string, Student>,因此必然會(huì)有一個(gè) Key 屬性,用來(lái)表示當(dāng)前分組下的入學(xué)年份是什么值。我們要進(jìn)行排序,因此要把這個(gè)值取出,并使用兩個(gè) let 從句將其解析為 int 類型,并參與 orderby 操作,進(jìn)行排序。最后,我們要把結(jié)果原封不動(dòng)返回,所以最后的 select 從句里寫的還是 currentGroup 而不是別的。如果這里寫的是別的的話,那么排序后就不再會(huì)把迭代結(jié)果給正確反饋出去了,那么 foreach 循環(huán)就不能得到正確的分組結(jié)果了。

另外,ascending 也是可以去掉的。

我們?cè)俅芜\(yùn)行程序,發(fā)現(xiàn)年份已經(jīng)成功分組后進(jìn)行升序排列結(jié)果:

4-2 對(duì) currentGroup 內(nèi)的元素進(jìn)行操作

前面的代碼我們已經(jīng)足夠我們?nèi)粘5氖褂昧?。不過(guò)很顯然,我們還能繼續(xù)排序。比如說(shuō)我要將每一個(gè)分組里的元素成員按姓名的字典序進(jìn)行升序排序,比如看前面的圖片可以發(fā)現(xiàn),2021 年入學(xué)的三位學(xué)生,藤宮香織以 S 字母開(kāi)頭,字典序比杰瑞的 J 和康娜醬的 K 都靠后,卻排在了前面。如果非要摳細(xì)節(jié)的話,我們確實(shí)可以去按照每一組的學(xué)生的名字來(lái)繼續(xù)排序一下。不過(guò),這怎么搞呢?

我們觀察一下原始代碼。顯然 select 從句后的表達(dá)式是 IGrouping<,> 類型的,因?yàn)檫@個(gè)變量就是原來(lái) group-by-into 后的那個(gè)變量。但是,因?yàn)?IGrouping<,> 接口是無(wú)法改變的,它只能通過(guò) group-by 從句執(zhí)行后反饋出來(lái),因此我們無(wú)法自己去使用這個(gè)接口來(lái)完成;否則你就得自己創(chuàng)建一個(gè)集合類型,去實(shí)現(xiàn)這個(gè)接口。顯然,太復(fù)雜了。

這里我們就需要一個(gè)小技巧了。我們?cè)囍ジ淖?select 從句的結(jié)果,使用一次嵌套查詢。因?yàn)?IGrouping<,> 接口允許我們迭代,因此我們可以繼續(xù)使用 foreach 循環(huán)來(lái)完成這項(xiàng)任務(wù)。

我相信你肯定看不習(xí)慣這個(gè)寫法,我們居然在 select 從句上使用了嵌套查詢。這是可以被接受的,而且 C# 確實(shí)允許我們這么做。我們?cè)?select 從句后跟了一個(gè)查詢表達(dá)式,將這個(gè)查詢表達(dá)式看成整體的話,就不難理解了:我想要迭代得到排序后的序列,所以我們不得不對(duì) IGrouping<,> 的可迭代結(jié)果進(jìn)行排序。

注意,排序后的結(jié)果是什么類型的呢?IOrderedEnumerable<> 類型的。這個(gè)接口和 IEnumerable<> 差別不大,所以你當(dāng)成 IEnumerable<> 的用法來(lái)用就行。不過(guò),這樣迭代后,有一個(gè)信息的損失——整個(gè)嵌套查詢返回的結(jié)果是一個(gè)可迭代的實(shí)例,但原來(lái)這里返回的是 IGrouping<,> 類型的實(shí)例,這個(gè)實(shí)例除了可以迭代以外,還有一個(gè) Key 屬性,用來(lái)取整個(gè)分組的分組依據(jù)的數(shù)值。我們改成嵌套查詢后就沒(méi)這個(gè)信息了。這咋辦呢?

沒(méi)關(guān)系,改成匿名類型實(shí)例返回就行了:

這樣就可以了。那么咋用呢?用法和之前的那個(gè)差不多,只不過(guò)這里是匿名類型,真正可以迭代的部分變?yōu)榱死锩娴?Elements 屬性的值。因此,我們只需要改一下內(nèi)層 foreach 循環(huán)的迭代對(duì)象即可:

是的,studentsGroup.Elements。我們?cè)賮?lái)運(yùn)行一下:

可以發(fā)現(xiàn),名字也排序了。這便是嵌套查詢位于 select 從句的特殊技巧。

當(dāng)然,你如果確實(shí)看不習(xí)慣的話,可以使用 let 從句為這個(gè)嵌套查詢結(jié)果單獨(dú)創(chuàng)建出變量來(lái),然后試著將其帶入結(jié)果返回:

這樣的話,效果是一樣的。

4-3 group-by-into 從句的拆解

之前說(shuō)過(guò) select-into 從句,我們說(shuō)明了一下它的形成緣由是為了內(nèi)聯(lián)查詢表達(dá)式,把查詢表達(dá)式給放在一起,into 從句則提供了接續(xù)的機(jī)會(huì)。那么對(duì)于 group-by-into 呢?我們能否做到這項(xiàng)任務(wù)呢?

答案是可以的。實(shí)際上,group-by-into 從句,以 into 作為分界線,我們可以拆解成兩部分:前面的內(nèi)容部分是一個(gè)查詢表達(dá)式,表示一個(gè)結(jié)果,而 into 后的則是另一個(gè)查詢表達(dá)式。比如前面的代碼:

這個(gè)如何去拆解呢?答案很簡(jiǎn)單,我們按 into 分界,前面的單獨(dú)定義為一個(gè)變量并賦值過(guò)去:

然后,將這個(gè)臨時(shí)結(jié)果帶到本在 into 從句后的代碼里作為迭代的開(kāi)始,而 from 后跟的是 into 從句的變量名,而 from-in 從句的 in 后的部分,則是這個(gè)前面分離出來(lái)的臨時(shí)變量 studentsGroupedByYearTemp

是的,這就是拆解的結(jié)果。當(dāng)然,拆解的方式是唯一的,因此要想合并起來(lái),只需要將剛才的拆解過(guò)程反向處理即可。來(lái)看一下完整的拆解結(jié)果吧:

好耶!

Part 5 為什么 group-by 要分開(kāi)寫,而 orderby 則是一起的?

那么,我來(lái)回答一下,為什么 orderby 是合在一起的關(guān)鍵字,但 group-by 是拆開(kāi)的兩個(gè)關(guān)鍵字。這個(gè)其實(shí)不太好說(shuō)明白。編譯器在分析代碼的時(shí)候,是為了盡量簡(jiǎn)單的方式來(lái)解決問(wèn)題的。C# 的編譯器的代碼分析能力有點(diǎn)恐怖了(恐怖到啥地步呢?恐怖到通過(guò)了圖靈測(cè)試)。

但是,編譯器會(huì)從優(yōu)解決翻譯和改寫代碼。group-by 從句執(zhí)行的操作顯然需要拆開(kāi),因?yàn)橛脩粲袝r(shí)候不一定非得去迭代返回整個(gè)對(duì)象本身,有些時(shí)候我只需要對(duì)象的取一部分信息即可???orderby 只是對(duì)于對(duì)象參與排序,而排序后你要干嘛都行,但都不屬于排序應(yīng)該做的事情。所以,從人為的邏輯上來(lái)說(shuō),orderby 只處理排序,跟你取里面啥東西沒(méi)有關(guān)系;但分組的話你可以設(shè)置取什么內(nèi)容和信息。

其次,從編譯器層面來(lái)說(shuō),orderby 的實(shí)現(xiàn)機(jī)制其實(shí)不難,你只需要實(shí)現(xiàn)一個(gè)排序就行了。可問(wèn)題就在于,orderby 是可以疊加排序依據(jù)的,也就是說(shuō),你可以 orderby s.A, s.B, s.C 甚至更多用逗號(hào)分開(kāi),但 group-by 做不到;相反,orderby 正是因?yàn)榭紤]到語(yǔ)法的復(fù)雜性,因此編譯器并沒(méi)有允許我們直接 orderby 的 order 和 by 兩個(gè)詞語(yǔ)中間插入東西。

詳情的話,請(qǐng)看一下霍姚遠(yuǎn)同學(xué)在 GitHub 上對(duì)這個(gè)疑問(wèn)的解答。

https://github.com/dotnet/csharplang/issues/3609#issuecomment-649205148

可能問(wèn)題看起來(lái)有點(diǎn)超綱,不過(guò)看下他的回答就可以了。

Part 6 總結(jié)

我們今天講解了如何使用 group-by 從句,以及它的執(zhí)行結(jié)果類型 IEnumerable<IGrouping<,>>。整個(gè)接口是允許迭代的,而每一個(gè)元素又是一個(gè)分了組之后的元素序列。里面的這個(gè) IGrouping<,> 接口可以理解為一對(duì)多的鍵值對(duì),在迭代里面的元素之外,還包含 Key 屬性,用來(lái)獲取這個(gè)分組下的分組依據(jù)的數(shù)值是什么。比如學(xué)生按班級(jí)分組的話,那么一年一班是一組,一年二班是一組,一年三班是一組,等等。那么這個(gè) Key 屬性就對(duì)應(yīng)一年一班、一年二班和一年三班等等。

接著,我們還說(shuō)了 group-by-into 從句,into 可以用來(lái)接續(xù),使得 group-by 的代碼可以延續(xù)繼續(xù)使用。接著我們還說(shuō)了怎么去處理和修改 into 后的這個(gè)變量里面包含的元素,比如變更序列的順序(用 orderby 排序序列后,直接改掉 select 從句的表達(dá)式,改用嵌套查詢表達(dá)式的模式來(lái)完成這個(gè)任務(wù))。

至此,group-by 就有了一個(gè)比較深入的認(rèn)知。下一節(jié)我們將講的是最后一個(gè)從句類型:join 從句。


第 101 講:C# 3 之查詢表達(dá)式(五):group 和 by 關(guān)鍵字的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
辽阳县| 定安县| 普宁市| 乌兰县| 莫力| 龙海市| 汤阴县| 石城县| 龙游县| 龙江县| 抚松县| 南开区| 安化县| 嘉定区| 唐海县| 高陵县| 错那县| 瑞昌市| 凉城县| 德钦县| 中牟县| 自贡市| 辽宁省| 莎车县| 偃师市| 普宁市| 双柏县| 鸡西市| 札达县| 古浪县| 旌德县| 临泽县| 焉耆| 闵行区| 上杭县| 比如县| 镇赉县| 胶南市| 花莲县| 岐山县| 加查县|