第 97 講:C# 3 之查詢表達(dá)式(一):LINQ 的概念以及 from 和 select 關(guān)鍵字
歡迎來(lái)到 LINQ 語(yǔ)法。LINQ 是 C# 里面最復(fù)雜的一個(gè)語(yǔ)法體系了,它的復(fù)雜程度估計(jì)就跟面向?qū)ο蟛畈惶?,是一個(gè)巨龐大的體系。下面我們將使用十多講來(lái)完成對(duì) LINQ 的介紹和全面講解。
今天我們要說(shuō)的是 LINQ 的基本概念,以及 LINQ 的簡(jiǎn)易使用。
Part 1 引例
在早期的 C# 里,很多東西使用起來(lái)都已經(jīng)比較方便了,但是有些時(shí)候,我們對(duì)集合的數(shù)據(jù)搜尋來(lái)說(shuō),仍舊有些不便??紤]一種情況。假設(shè)我要獲取一個(gè)數(shù)組里的所有奇數(shù),我們的寫(xiě)法是這樣的:
foreach
循環(huán),我們可以得到所有 array
封裝了之后,調(diào)用起來(lái)就簡(jiǎn)單多了:
我們使用傳入 Lambda 表達(dá)式的方式,來(lái)完成搜尋奇數(shù)的目的。這里我們用到了新語(yǔ)法有:
C# 2 的靜態(tài)類(lèi);
C# 2 的泛型;
C# 2 的
yield return
表達(dá)式;C# 3 的擴(kuò)展方法;
C# 3 的 Lambda 表達(dá)式。
試想一下,Lambda 表達(dá)式是挺好用,但有沒(méi)有稍微優(yōu)雅一點(diǎn)的、專(zhuān)門(mén)用來(lái)搜索集合元素的語(yǔ)法來(lái)處理這個(gè)?答案是有的,下面我們就來(lái)說(shuō)一下,C# 3 的 LINQ。
通過(guò) C# 3 的 LINQ,我們可以這么寫(xiě)代碼:
from e in array where e % 2 == 1 select e
是一個(gè)表達(dá)式。這個(gè)表達(dá)式有些長(zhǎng),不過(guò)挺有意思的。而至于 from
、in
、select
和 where
等等關(guān)鍵字,是我們這里要說(shuō)的東西。
Part 2 LINQ 是什么?
LINQ,全稱(chēng) Language-Integrated Query,直接翻譯出來(lái)叫做“集成語(yǔ)言查詢”。這個(gè)是什么意思呢?查詢(Query)這個(gè)詞語(yǔ),在整個(gè) IT 界都小有名氣,它表示搜索我們需要的東西的行為。查詢是一個(gè)術(shù)語(yǔ)詞,大概可以理解為搜索的意思。
所謂的“集成語(yǔ)言查詢”,這個(gè)應(yīng)該理解為“語(yǔ)言集成的查詢機(jī)制”,這個(gè)“語(yǔ)言”指的是 C# 這個(gè)編程語(yǔ)言,而集成指的是在 C# 這門(mén)語(yǔ)言里有內(nèi)置語(yǔ)法,我們可以通過(guò)這樣的語(yǔ)法來(lái)完成查詢操作和機(jī)制。
C# 的 LINQ 包含如下的一些關(guān)鍵字:
from
:用來(lái)表示迭代變量;in
:表示迭代的集合;select
:表示我們需要去讓什么變量作為返回;into
:表示繼續(xù)使用迭代結(jié)果;where
:表示條件;orderby
:表示排序集合;ascending
:表示集合升序排序;descending
:表示集合降序排序;group
:表示分組集合;by
:表示分組集合的依據(jù);join
:表示在集合配對(duì)和連接別的數(shù)據(jù);on
:表示join
連接期間的條件;equals
:表示連接的相等的成員比較。
可以看出,關(guān)鍵字使用情況相當(dāng)多。正是因?yàn)檫@樣的復(fù)雜性,所以 LINQ 三言兩語(yǔ)肯定是說(shuō)不完的。今天我們要介紹的是前面四個(gè)關(guān)鍵字:from
、in
、select
和 into
語(yǔ)句。
Part 3 from-in-select
查詢
3-1 語(yǔ)法
下面我們針對(duì)于集合迭代過(guò)程來(lái)簡(jiǎn)易說(shuō)明一下使用。先介紹的是最基礎(chǔ)的 from-in-select
語(yǔ)句。
from-in-select
語(yǔ)句,用連字符連起來(lái)的意思就是說(shuō),它們?nèi)齻€(gè)關(guān)鍵字是順次書(shū)寫(xiě)的,語(yǔ)法是這樣的:
from-in-select
語(yǔ)句稱(chēng)為映射表達(dá)式
我們直接在 select
從句的后面寫(xiě)上 element + 1
作為表達(dá)式,這表示我們獲取的元素是 element + 1
作為結(jié)果;而 from
和 in
這一部分則表示的是集合迭代的過(guò)程,書(shū)寫(xiě)的寫(xiě)法總是 from
后跟上迭代變量的名稱(chēng),而 in
后跟上的是集合變量。
這樣的語(yǔ)法看不習(xí)慣,可以使用 foreach
循環(huán)進(jìn)行等價(jià)轉(zhuǎn)換:
是的。就等價(jià)于這個(gè)。foreach (var element in array)
被代替為 from element in array
,而 yield return element + 1
被代替為 select element + 1
。因此,你可以說(shuō),from-in
部分表示 foreach
循環(huán)的意思,而 select
部分表示的是 yield return
的意思。
不過(guò),from-in-select
是不可分割的,也就是說(shuō)一旦出現(xiàn)就必須全都包含,不能缺少任何其中的一部分。比如說(shuō),只有 from-in
的語(yǔ)句是錯(cuò)誤的:
foreach
循環(huán)后,這個(gè)方法的名稱(chēng) AddOne
返回的結(jié)果類(lèi)型是 IEnumerable<int>
。是的,這一點(diǎn)需要你注意。因?yàn)榈葍r(jià)的轉(zhuǎn)換的關(guān)系,from-in-select
整個(gè)部分我們稱(chēng)為一個(gè)表達(dá)式(因?yàn)樗梢詫?xiě)在等號(hào)右邊,進(jìn)行賦值給左邊的變量),而這個(gè)表達(dá)式的結(jié)果類(lèi)型是 IEnumerable<>
類(lèi)型的。換句話說(shuō),實(shí)際上這里的 var
就表示 IEnumerable<int>
不過(guò),有些時(shí)候比較長(zhǎng),因此你可以換行:
都是可以的。不過(guò)我們建議你換行的時(shí)候?qū)?from
和 select
單獨(dú)作為一行,而不要斷開(kāi) from
和 in
,因?yàn)樗鼈儽坏葍r(jià)為 foreach
循環(huán)的聲明的頭部了,它們是不可拆分的。
我們把 from element in array select element + 1
稱(chēng)為一個(gè)查詢表達(dá)式(Query Expression),它是一個(gè)表達(dá)式,而且功能是用來(lái)查詢,因此叫做查詢表達(dá)式;另外,查詢表達(dá)式不只是 from-in-select
表達(dá)式,還有別的,后面我們會(huì)慢慢接觸到它們。
另外,我們把 from
后跟的變量也稱(chēng)為迭代變量(Iteration Variable),不過(guò)在 C# 里,它只在這個(gè)表達(dá)式里才能使用,比大括號(hào)的級(jí)別還要小,因此我們把這樣的變量也稱(chēng)為范圍變量(Range Variable)。
3-2 select
從句就用迭代變量的情況
如果使用 select
的時(shí)候,我們只寫(xiě)本身的話,會(huì)如何呢:
from
里聲明的 element
變量直接寫(xiě)在 select
后了。如果把它轉(zhuǎn)換為 foreach
這不是多此一舉嗎?我故意使用 foreach
將數(shù)組的每一個(gè)元素都迭代出來(lái),結(jié)果我又使用 yield return
把每一個(gè) element
給整合起來(lái)。這種寫(xiě)法也不是錯(cuò)的,不過(guò)沒(méi)有必要這么寫(xiě),對(duì)吧。
3-3 select
從句用常量的情況
考慮下面的查詢表達(dá)式:
element
因此,我們也盡量避免這種寫(xiě)法。真要說(shuō)這個(gè)迭代的結(jié)果是什么,那就只能表示成“和集合元素?cái)?shù)量一樣多的 42 構(gòu)成的序列”。
3-4 使用查詢表達(dá)式需要引用 System.Linq
命名空間
在使用上述這樣的查詢表達(dá)式而不是 foreach
循環(huán)的時(shí)候,我們需要在文件最開(kāi)頭補(bǔ)充 using System.Linq;
命名空間的引用,原因今天講不了,這個(gè)我們將在后面說(shuō)到。
Part 4 顯式指定迭代變量的類(lèi)型
可以從前文給出的語(yǔ)法規(guī)則看出,映射表達(dá)式的 from
后是有一個(gè)變量類(lèi)型可以寫(xiě)的,不過(guò)它被標(biāo)記了 ?
說(shuō)明可以沒(méi)有。前面講的是沒(méi)有的情況,下面我們來(lái)說(shuō)一下帶有變量類(lèi)型的情況。
考慮使用 ArrayList
這樣的集合。如果這樣的代碼在迭代的時(shí)候,將會(huì)產(chǎn)生錯(cuò)誤:
但是,這樣書(shū)寫(xiě)有一個(gè)問(wèn)題。ArrayList
類(lèi)型是很早的數(shù)據(jù)類(lèi)型,它雖然是一個(gè)集合,但里面的元素是 object
類(lèi)型的,雖然我們知道,我們這個(gè)集合里只存 int
元素,但因?yàn)樗陨碇粚?shí)現(xiàn)了 IEnumerable
接口,而沒(méi)有實(shí)現(xiàn)泛型的版本(它自己在沒(méi)有泛型之前就有了),因此 from-in
在書(shū)寫(xiě)的時(shí)候就會(huì)失敗。所以,上面的代碼是不合理的。
你想想,你轉(zhuǎn)換為 foreach
后,代碼是不是這樣:
e
是什么類(lèi)型呢?object
e + 1
就不對(duì)了。于是,我們會(huì)試著改變 object
我們通過(guò)顯式指定數(shù)據(jù)類(lèi)型 int
來(lái)代替 object
,這樣我們就可以在 foreach
的底層自動(dòng)進(jìn)行 object
到 int
的強(qiáng)制轉(zhuǎn)換,因?yàn)槲覀冎烂恳粋€(gè)元素都是 int
類(lèi)型,所以這么寫(xiě)是可以的。于是,這樣的聲明是合理的。
但仔細(xì)想想,前面我們介紹的例子迭代的是 int[]
,也就是說(shuō),foreach
循環(huán)我們完全不必聲明它迭代變量的類(lèi)型,寫(xiě)成 var
編譯器也知道;但是 ArrayList
不行,不寫(xiě)就不知道具體類(lèi)型,因此我們必須強(qiáng)制寫(xiě)出來(lái)元素自己的實(shí)際類(lèi)型,然后做一次隱式的強(qiáng)制轉(zhuǎn)換,是的,隱式的強(qiáng)制轉(zhuǎn)換。隱式指的是它在底層才知道有這個(gè)轉(zhuǎn)換邏輯,而直接看 foreach
是看不太出來(lái)的;而強(qiáng)制轉(zhuǎn)換是背后執(zhí)行的邏輯和機(jī)制。
那么,LINQ 里要怎么做呢?from e in list select e + 1
嗎?肯定不行。于是 LINQ 允許我們?cè)诘兞康淖筮吪渖纤膶?shí)際類(lèi)型,表示進(jìn)行強(qiáng)制轉(zhuǎn)換的具體類(lèi)型。于是,改寫(xiě)為這樣就可以了:
是的,from int e in list
。
Part 5 疊加 from-in
從句
from-in
從句是可以疊加的??紤]一下,我有兩個(gè)集合需要迭代,然后湊一對(duì),我們可以這么做:
我們可以構(gòu)成一個(gè)數(shù)對(duì),然后用匿名類(lèi)型表示出來(lái),然后兩層循環(huán),將兩個(gè)集合的元素兩兩組合。C# 的 LINQ 也能做這個(gè)事情:
from-in
從句,就可以達(dá)到兩層循環(huán)的效果。這就是 LINQ 的魅力之處。當(dāng)然,from-in
是的,匿名類(lèi)型是不用寫(xiě)出來(lái)屬性名稱(chēng)的,C# 的匿名類(lèi)型具有屬性名推斷的功能,如果你寫(xiě)的匿名類(lèi)型的表達(dá)式是
new { a, b }
的話,那么生成的對(duì)應(yīng)匿名類(lèi)型的具體類(lèi)型,會(huì)直接使用變量名稱(chēng)a
和b
表示這兩個(gè)實(shí)際屬性,而如果是new { 1 }
就不行了,因?yàn)?1 是常量,它沒(méi)有對(duì)應(yīng)的變量名稱(chēng),因此無(wú)法將其當(dāng)作屬性名使用,這種情況下就必須寫(xiě)屬性名稱(chēng)了。另請(qǐng)注意
new[] { a, b }
和new { a, b }
的區(qū)別。new[] { a, b }
是數(shù)組的隱式類(lèi)型的初始化器,因?yàn)橛袀€(gè)方括號(hào);而new { a, b }
沒(méi)有方括號(hào)了,因此會(huì)被視為匿名類(lèi)型的表達(dá)式。
Part 6 嵌套查詢和 into
從句
有些時(shí)候,foreach
僅僅是上面那樣的話,就顯得比較簡(jiǎn)單了,一些復(fù)雜的東西可能前面的內(nèi)容就做不到了。下面我們來(lái)看一些靈活的處理。
6-1 嵌套查詢
考慮一種情況,我們要通過(guò) from-in-select
表達(dá)式得到一個(gè)新集合,然后將這個(gè)集合再一次使用和映射起來(lái)。我們可以這么做:
是的,我們迭代 c1
集合,得到每一個(gè)元素的平方根,然后組成新的序列;與此同時(shí)我們迭代 c2
集合,得到這個(gè)序列每一個(gè)元素的平方根。最后,我們使用疊加的 from-in
從句,來(lái)進(jìn)行兩兩配對(duì),最后得到每一組兩個(gè)元素的平方根的和。
這里稍微提一句。注意
temp1
和temp2
的查詢表達(dá)式寫(xiě)法。在from
后,兩個(gè)查詢表達(dá)式用的是相同的標(biāo)識(shí)符t
,但是因?yàn)榍拔恼f(shuō)過(guò),查詢表達(dá)式內(nèi)的變量我們叫迭代變量,它僅用于轉(zhuǎn)化為foreach
循環(huán)后的這個(gè)循環(huán)體內(nèi)部使用,因此在查詢表達(dá)式里,迭代變量?jī)H可以提供給整個(gè)查詢表達(dá)式的范圍使用,超出這個(gè)范圍的任何其它的地方都是無(wú)法“看”到它的。因此,兩個(gè)t
并不會(huì)沖突,正是因?yàn)槿绱?,我們才叫它范圍變量,因?yàn)樗却罄ㄌ?hào)的級(jí)別還要小,只在表達(dá)式的小范圍里才能隨便使用它。
這樣的寫(xiě)法是可以,不過(guò) temp1
和 temp2
不必顯式用變量寫(xiě)出來(lái),于是我們可以內(nèi)聯(lián)到同一個(gè)語(yǔ)句里去。
想一想,from-in
的 in
后面是不是就是集合?。恳胧且粋€(gè)集合,首先是不是得至少可以允許它進(jìn)行 foreach
循環(huán)???有了這個(gè)循環(huán)我們才能將查詢表達(dá)式和 foreach
進(jìn)行等價(jià)轉(zhuǎn)換。那么至少這個(gè)集合就得是 IEnumerable<>
或者是 IEnumerable
的。欸,等下,是不是很熟悉?是的,查詢表達(dá)式自己的結(jié)果就是這兩個(gè)接口類(lèi)型的。那如果我直接將結(jié)果內(nèi)聯(lián)寫(xiě)到 in
的后面不就可以了?
in
這樣也是可以的。
我們把第 2、3 行的 in
關(guān)鍵字后的查詢表達(dá)式稱(chēng)為嵌套查詢表達(dá)式(Nested Query Expression),它嵌套在整個(gè) from-in-from-in-select
表達(dá)式的里面。不過(guò)注意使用和計(jì)算順序,先計(jì)算出內(nèi)層的結(jié)果,然后才能執(zhí)行和得到整個(gè) from-in-from-in-select
表達(dá)式的結(jié)果。
6-2 into
從句
我們可以使用 select-into
從句來(lái)完成對(duì)臨時(shí)查找的結(jié)果繼續(xù)進(jìn)行迭代操作的“接續(xù)”。考慮一下前面的代碼,我們發(fā)現(xiàn)一個(gè)通用的結(jié)論:select
總是被放在末尾。因?yàn)?select
用來(lái)映射結(jié)果,因此它肯定只能放末尾;但有些時(shí)候也不一定。
考慮一種情況,我給出了一系列的學(xué)生信息:
我給出了假機(jī)器人鷺宮詩(shī)織、真機(jī)器人東云名乃、蠟筆山小新、湯姆貓、杰瑞鼠、托爾醬、康娜醬、下弦之一魘夢(mèng)、平澤唯和阿梓喵這幾個(gè)學(xué)生的基本信息(說(shuō)這段好像沒(méi)啥意思,但是我就是故意說(shuō)的,我二次元濃度真的不高)。
我想將列表的所有學(xué)生都看一遍,看看是不是都成年了,然后給每一個(gè)學(xué)生都配上結(jié)果。我不管你是不是來(lái)自異次元,咱只看地球上的標(biāo)準(zhǔn)——看這些學(xué)生是不是滿 18 歲。那么代碼應(yīng)該是這樣:
是的,這里我們使用到了一個(gè)新的語(yǔ)法:select-into
從句。請(qǐng)注意第 3 行。代碼使用到的語(yǔ)句寫(xiě)法是 select new { ... } into ...
,這表示什么呢?這表示的是,我將前面的學(xué)生的信息,按 new { ... }
這個(gè)匿名類(lèi)型的表達(dá)式轉(zhuǎn)換一下,然后將轉(zhuǎn)換的這個(gè)結(jié)果,使用 temp
表示成變量。
換句話說(shuō),select-into
語(yǔ)句允許我們定義臨時(shí)變量在中間。它將 select
后的表達(dá)式進(jìn)行中間化,避免 select
一定要放在最后的問(wèn)題。
當(dāng)然了,上述這樣的代碼也可以被精簡(jiǎn)一下:
是的,這樣寫(xiě)其實(shí)也沒(méi)毛病,只是說(shuō)初學(xué)的話可能不太好理解。
6-3 “副作用”:into
從句會(huì)阻斷范圍變量的使用范圍
這是一個(gè)比較“奇怪”的設(shè)計(jì)??赡苣銜?huì)覺(jué)得是這樣,不過(guò) select-into
設(shè)計(jì)出來(lái)是別有用途的,但目前我們還沒(méi)辦法說(shuō),下一講內(nèi)容我們會(huì)講解 let
關(guān)鍵字,到時(shí)候我們就會(huì)說(shuō)明 select-into
這種副作用的真正原因。
比如下面這樣寫(xiě)代碼,就是不對(duì)的:
new { digit, d1 }
是不對(duì)的寫(xiě)法,因?yàn)槭褂?d1
變量是不可以的,因?yàn)榍懊嬗?select-into
從句,這意味著 into digit
之前定義的變量就不能在 into digit
之后使用了。
Part 7 查詢表達(dá)式當(dāng)成參數(shù)和臨時(shí)表達(dá)式使用
前面我們就說(shuō)過(guò),查詢表達(dá)式是一個(gè)表達(dá)式,它是 IEnumerable<>
類(lèi)型的。因此,任何可以用表達(dá)式的地方,只要類(lèi)型對(duì)得上,這樣的語(yǔ)法就可以無(wú)處不在。
比如 string.Join
方法。這個(gè)方法用來(lái)給序列的元素和元素之間插入分隔符。假設(shè)我有一組元素,要在每個(gè)元素之間插入逗號(hào),我們可以這么寫(xiě)代碼:
是的,我們直接將查詢表達(dá)式當(dāng)成參數(shù)傳入到第二個(gè)位置上去。這是允許的寫(xiě)法。
另外,我們也可以使用前面講過(guò)的擴(kuò)展方法來(lái)拓展用法。假設(shè)我想要獲取整個(gè)集合的第一個(gè)元素的話:
我們這里有了擴(kuò)展方法了。下面我們就可以這么做了:
我們使用查詢表達(dá)式得到所有學(xué)生的名字,然后使用 First
擴(kuò)展方法的語(yǔ)法來(lái)獲取這個(gè)序列里的第一個(gè)元素。因此這個(gè)整個(gè)計(jì)算過(guò)程的結(jié)果,其實(shí)就是名字序列里的第一個(gè),因此 s
就是這個(gè)名字,是 string
類(lèi)型的。
稍微注意一下的是,由于查詢表達(dá)式的完整性和復(fù)雜性,我們必須在調(diào)用擴(kuò)展方法之前,使用小括號(hào)把整個(gè)查詢表達(dá)式給括起來(lái),這是在這種用法下唯一需要我們注意的地方。
Part 8 總結(jié)
今天我們講解了使用 from
、in
、select
關(guān)鍵字來(lái)完成一些操作,知道了什么叫做查詢表達(dá)式,范圍變量的具體范圍又是哪些。另外,我們?cè)谶@一節(jié)里大量使用到匿名類(lèi)型,也對(duì)我們之前學(xué)習(xí)匿名類(lèi)型來(lái)說(shuō)有了一個(gè)比較合理的認(rèn)知。
下一節(jié)我們將給大家介紹 let
從句,用來(lái)臨時(shí)定義變量。