第 99 講:C# 3 之查詢表達(dá)式(三):where 關(guān)鍵字
今天我們要講一個(gè)新的關(guān)鍵字:where
。它用來(lái)篩選,可以得到只滿足條件的序列。
Part 1 引例
我們還是使用之前的學(xué)生的序列來(lái)說(shuō)明用法。
假設(shè)我要獲取這些學(xué)生里成年了的人。我不管它們生活的那個(gè)異世界是怎么規(guī)定和計(jì)算年齡的,我們假設(shè)把傳入的第三個(gè)參數(shù)(假設(shè)叫它 Age
屬性)當(dāng)成年齡數(shù)值,然后獲取出所有年齡都大于 18 歲的。
我們之前的關(guān)鍵字都無(wú)法做到,因?yàn)樗鼈兌疾痪哂泻Y選功能。今天我們要介紹的 where
關(guān)鍵字就表示篩選。用法很簡(jiǎn)單,當(dāng)成類似 let
從句那樣,插入到中間當(dāng)成篩選就可以了。
where student.Age >= 18
來(lái)計(jì)算和篩選對(duì)象。和之前一樣,我們將 from
后的變量當(dāng)成迭代變量,然后 where
就好比一個(gè) if
條件一樣。所以這個(gè)語(yǔ)句等價(jià)的 foreach
很簡(jiǎn)單,對(duì)吧。這就是 where
從句的用法。講完了。
Part 2 where
從句也可以多次使用
C# 的查詢表達(dá)式靈活就靈活在,它可以隨便用而不去考慮底層是怎么去實(shí)現(xiàn)的。
如果你用熟練了的話,你就可以寫出這么長(zhǎng)的查詢表達(dá)式。我們挨個(gè)來(lái)分析一下。
from student in students
表示在迭代 students
集合,然后每一個(gè)元素我們用 student
臨時(shí)表示一下。它類似 foreach
循環(huán)的迭代變量;let age = student.Age
在定義臨時(shí)變量,表示 age
是這個(gè)學(xué)生的年齡信息,用臨時(shí)變量表達(dá)起來(lái);where age >= 18
表示獲取年齡大于等于 18 歲的;let total = student.Chinese + student.English + student.Math
表示計(jì)算這個(gè)學(xué)生的總成績(jī);let average = total / 3.0
表示計(jì)算這個(gè)總分除以 3 的結(jié)果,即平均分;where average >= 60
又是一次篩選,表示學(xué)生的平均分是否大于等于 60;let name = student.Name
表示臨時(shí)變量的定義,得到這個(gè)學(xué)生的名稱;最后的 select name
表示獲取這個(gè)名字作為迭代結(jié)果。
因此,整個(gè)表達(dá)式可以等價(jià)于下面這個(gè) foreach
循環(huán):
是的。let
等價(jià)于一個(gè)變量的定義,where
則直接改成 if
即可。注意兩次 where
從句,先出現(xiàn)的是外層的 if
,而出現(xiàn)的則是內(nèi)層的 if
。
如果你覺(jué)得大括號(hào)導(dǎo)致的層次級(jí)別太多,你可以取反一下邏輯:
我們直接取反 if
條件,然后直接把后面的代碼減少一次縮進(jìn),再把原來(lái)直接跟在 if
后的執(zhí)行語(yǔ)句改成 continue;
即可。順帶一說(shuō),這也是編程的時(shí)候最習(xí)慣的書寫模式了。我們總是將每次得到的、不滿足條件的對(duì)象通過(guò) continue
語(yǔ)句直接跳過(guò)計(jì)算,代碼可以少一些縮進(jìn),在代碼效率保證了的前提下還減少了縮進(jìn),增強(qiáng)了可讀性。
另外,從這樣的 foreach
循環(huán)的等價(jià)代碼理解可以看出,多次 where
從句篩選出來(lái)的結(jié)果是必須對(duì)這些給出的條件全部都滿足的迭代對(duì)象。
Part 3 緊挨著的 where
從句
剛才我們說(shuō)到一個(gè)結(jié)論。where
從句表示一種篩選過(guò)程,當(dāng)條件滿足的時(shí)候,這樣的對(duì)象才可能被迭代返回出來(lái)。如果多次條件篩選的話,那就相當(dāng)于是 &&
處理一樣的效果。
比如下面這樣的代碼:
let
從句,實(shí)際代碼和原來(lái)的邏輯是一樣的,都是“獲取所有成年并且及格的學(xué)生的名字”。不過(guò),這次我們連著使用了兩次 where
從句。這樣其實(shí)是不好的。因?yàn)閮蓚€(gè) where
if
。如果 if
出現(xiàn)嵌套,我們就可以認(rèn)為,兩個(gè) if
可以用 &&
連起來(lái)。因?yàn)閮蓚€(gè) if
where
從句也可以這么做。我們將緊挨著的兩個(gè)甚至更多的 where
從句里面的條件全部使用 &&
連起來(lái),然后直接寫成一個(gè) where
當(dāng)然,如果你不考慮性能的話,其實(shí)寫成原來(lái)的寫法也沒(méi)啥問(wèn)題,兩種寫法執(zhí)行出來(lái)的結(jié)果是一致的,只是性能上有點(diǎn)區(qū)別罷了。
但請(qǐng)注意,連續(xù)出現(xiàn)的 where
是 &&
的邏輯,而不是 ||
的邏輯。你不能把兩個(gè)應(yīng)該用 ||
連起來(lái)的條件拆解為兩個(gè)連續(xù)的 where
從句表達(dá)出來(lái),也不能將兩個(gè)連續(xù)的 where
從句給的條件用 ||
連起來(lái)。初學(xué)這里的時(shí)候經(jīng)常有同學(xué)會(huì)忘記這一點(diǎn),導(dǎo)致邏輯出現(xiàn)問(wèn)題。
Part 4 不太算是題外話的題外話
4-1 from
從句可以不挨在一起用
我們?cè)谡f(shuō)查詢表達(dá)式的最開(kāi)始的內(nèi)容里就說(shuō)過(guò),from
是可以多次使用的,它就等價(jià)于多層的 foreach
循環(huán)。不過(guò),前面的例子還不夠多,所以我們的想象力限制了 from
的用法。實(shí)際上,from
是可以分開(kāi)用的,也就是說(shuō),有一個(gè) from
從句后,跟上一些別的從句(比如 let
、where
從句什么的)之后,然后再來(lái)一個(gè) from
從句,也是可以的。只不過(guò)之前沒(méi)有 where
從句,所以不太好對(duì)這種情況進(jìn)行舉例說(shuō)明。下面我們來(lái)介紹一種用法來(lái)這么去使用 from
。
考慮一種情況,我現(xiàn)在想對(duì)整個(gè)學(xué)生集合進(jìn)行一男一女的 CP 配對(duì)操作。我們可以這么去寫代碼:
from
從句后跟了兩次 let
從句,然后才是新的 from
。這也是允許的。它的等價(jià) foreach
where
寫到一起,用 &&
下面我們來(lái)說(shuō)說(shuō),這些語(yǔ)句都是什么意思。
我們?cè)俅握瞻嵘厦娴牟樵儽磉_(dá)式:
我們要習(xí)慣去看查詢表達(dá)式而不是 foreach
循環(huán)的等價(jià)寫法。如果每次都要自己腦補(bǔ) foreach
循環(huán)然后去反推查詢表達(dá)式的話,學(xué)習(xí)起來(lái)就比較慢,效率就不太高了。
我們首先迭代了 students
序列,我們將每一個(gè)學(xué)生的名字和性別取出來(lái)留著稍后使用。接著我們?cè)俅沃匦碌?students
序列。注意這次我們寫在上面 from
的下方,因此它等價(jià)于的是 foreach
的嵌套。你想想看,where
多次用的話,前面的 where
改成了外層的 if
,而后面的 where
則改成了內(nèi)層的 if
。所以 from
的話,也是這個(gè)道理。
兩層嵌套的 foreach
循環(huán)一般都是用來(lái)進(jìn)行兩兩組合的,剛好我們這里就需要一男一女組合,所以完全符合我們這里的需求。繼續(xù)往下看,在得到兩位學(xué)生(兩個(gè) students
迭代期間的臨時(shí)變量 student1
和 student2
)的性別和名字后,我們需要判斷這兩個(gè)對(duì)象是否需要配對(duì)。因?yàn)槲覀冎恍枰荒幸慌M合,因此我們需要判斷性別和名字兩個(gè)屬性的數(shù)值。
為什么連名字也要判斷呢?因?yàn)閮蓪忧短椎?foreach
循環(huán)都迭代的是相同的序列,那么完全有可能在迭代期間遇到兩個(gè)循環(huán)迭代到同一個(gè)對(duì)象的情況。為了避免程序的 bug,我們必須排除掉這種特殊情況。因此可以在第 8 行的篩選條件里看到 name1 != name2
的判斷。當(dāng)然,一男一女就意味著兩個(gè)人的性別是不一致的,所以 gender1 != gender2
就比較好理解了。
當(dāng)然,如果你想更加巧妙地使用代碼的話,你可以這么去處理性別比較。假設(shè)我們這個(gè)例子里
Gender
是個(gè)枚舉類型,只包含Male
和Female
兩個(gè)數(shù)值情況的話,那么我們很容易會(huì)認(rèn)為Male
對(duì)應(yīng)特征數(shù)值 0、Female
對(duì)應(yīng)特征數(shù)值 1(當(dāng)然,你要反過(guò)來(lái)也行)。那么,要想保證兩個(gè)人的性別不一致,那么我們可以使用異或運(yùn)算。異或運(yùn)算恰好保證的就是兩個(gè)數(shù)值一致的時(shí)候?yàn)?0;反而不一致的時(shí)候是 1。因此,gender1 != gender2
也可以寫成(gender1 ^ gender2) != 0
。呃好像代碼更長(zhǎng)了……我只是說(shuō)有這么個(gè)用法,是這么個(gè)寫法,是這個(gè)意思。
那么,第 9 行的 where
條件又是個(gè)什么情況呢?之所以沒(méi)有內(nèi)聯(lián)到上一個(gè) where
從句里去,是因?yàn)樗枰獑为?dú)說(shuō)明邏輯。反正這么寫和寫在一起也沒(méi)有運(yùn)行上的差別。這個(gè)第 9 行的代碼,其實(shí)是為了保證我們給出的 student1
(也就是外層 foreach
循環(huán)的迭代變量)一定是個(gè)男學(xué)生。因?yàn)榍耙粋€(gè) where
從句(第 8 行)判斷了兩個(gè)學(xué)生不同性別,因此 student1
保證了是男學(xué)生的話,那么 student2
就肯定是女學(xué)生了。這樣我們就可以大大方方在第 10 行的 select
語(yǔ)句里直接指定 Male
和 Female
屬性到匿名類型的初始化器里去了。這就是為什么需要額外判斷一下 student1
是不是男性的原因——為了保證一男一女表達(dá)出來(lái)的時(shí)候,先給的 student1
一定是男生,而后給的 student2
一定是女生。
下面請(qǐng)你自己思考一個(gè)問(wèn)題。這樣篩選會(huì)不會(huì)漏掉組合的情況?答案自己想哈,我就不給出解釋了。
4-2 巧用匿名類型簡(jiǎn)化多次定義的 let
從句
例如上面這樣的定義,因?yàn)槲覀儽仨毰袛?Name
和 Gender
屬性,因此我們需要用四次 let
從句來(lái)獲取數(shù)值。這樣定義是沒(méi)有問(wèn)題,但是顯然多次定義可以簡(jiǎn)化一下。
我們可以巧妙利用匿名類型來(lái)完成這個(gè)任務(wù)。我們將同一個(gè)對(duì)象的定義過(guò)程放在一個(gè)匿名類型的初始化器里賦值,然后賦值給一個(gè)變量就行了:
我們注意一下修改的地方。我們刪除了賦值過(guò)程,取而代之的是新的賦值過(guò)程。注意賦值的對(duì)象發(fā)生了變化:原來(lái)是屬性的賦值,現(xiàn)在是匿名類型的賦值。這樣的賦值我們只需要寫兩次 let
從句。這樣可以定義和賦值到同一個(gè)變量里去。接著,我們判斷名字和性別是否都不同,它被改成了 pair1
和 pair2
兩個(gè)匿名類型之間的屬性的比較。最后我們的 where
要保證第一個(gè)對(duì)象必須是男性,所以現(xiàn)在我們也需要同步地改變寫法,改用 pair1
來(lái)比較。
注意改變后的判斷語(yǔ)句可能會(huì)變得更長(zhǎng)。這也是沒(méi)辦法的事情。另外,結(jié)合之前我們學(xué)了匿名類型的語(yǔ)法的知識(shí)點(diǎn),我這里留一個(gè)問(wèn)題希望各位思考一下。
請(qǐng)問(wèn),改寫的代碼
where pair1.Name != pair2.Name && pair1.Gender != pair2.Gender
(第 11 行)能否寫成!pair1.Equals(pair2)
或pair1 != pair2
?請(qǐng)說(shuō)明理由。
Part 5 總結(jié)
總的來(lái)說(shuō),我們介紹了 where
的用法,它表示篩選條件。多個(gè)挨著的 where
可以合并到一起,用 &&
連起來(lái)。
下面我們將介紹一個(gè)新的從句:orderby
從句。