第 106 講:C# 3 之查詢表達式(十):from 和 select 的底層原理
今天是 5?1 勞動節(jié),在此祝各位朋友節(jié)日快樂。如果還要上班的朋友們請注意勞逸結(jié)合和身體健康。
相信各位已經(jīng)對查詢表達式比較熟悉了,雖然達不到靈活運用但最起碼也是有點療效基本的可以寫了。從今天開始,我們將給大家逐個說明查詢表達式的實現(xiàn)原理(也就是說查詢表達式能否和我們現(xiàn)有的語法關(guān)聯(lián)起來,做等價轉(zhuǎn)換)。
Part 1 什么?擴展方法?就這么簡單?
很高興跟大家說明原理。實際上,查詢表達式完全可以等價轉(zhuǎn)換成擴展方法的調(diào)用。咱先不考慮看不看得懂,咱們先來寫幾個等價轉(zhuǎn)換的前后的對照例子作參考。
例子 1:獲取序列每個元素加 3 后的序列。
from
挨著就會被識別為 SelectMany
之類的。
下面我們將率先給大家解釋 from
和 select
的底層調(diào)用規(guī)則,以及它的翻譯邏輯,這樣對于你以后使用查詢表達式來說,如果覺得不習(xí)慣,可以使用方法調(diào)用的等價寫法。
Part 2 Select
方法組
2-1 select
和 Select
方法的等價轉(zhuǎn)換
要介紹翻譯后的樣子,我們先要介紹的是 Select
方法組。為什么說是方法組呢?因為 Select
方法重名的方法有兩個。
Select(集合, 迭代變量 => 結(jié)果表達式)
Select(集合, (迭代變量, 索引) => 結(jié)果表達式)
其中的第一個,是我們要使用的方法,另外一個因為不太常用所以就當(dāng)順帶一說吧。
這個方法傳入兩個對象當(dāng)參數(shù),第一個是我們要迭代的序列,第二個則是我們要映射的委托實例。
在 LINQ 里,我們還是盡量說成 Lambda 而不是去說委托對象,因為說委托不太好理解,直接說是 Lambda 執(zhí)行,在 LINQ 也最常用也最好用。比如從現(xiàn)在開始,我就把類似這樣的“委托類型的參數(shù)”直接說成 Lambda。
第一個參數(shù)很好理解,就是參數(shù)實例本身。因為它是擴展方法(該參數(shù)有 this
修飾符),因此可以使用實例前置來達到好用的寫法格式;而第二個參數(shù)怎么用呢?對于 Lambda,我們知道需要帶有參數(shù)和執(zhí)行過程兩部分。對于 Select
而言, 這里的 Lambda 自身需要一個參數(shù),指的是我們迭代過程期間的臨時變量。它就相當(dāng)于我們寫成查詢表達式的時候的這個 from
后面的這個變量名。而 Lambda 后跟的執(zhí)行內(nèi)容,必須返回一個結(jié)果,該結(jié)果可以任何我們可以使用的數(shù)據(jù)類型。那么,第二個參數(shù)就表示的是“期間我們應(yīng)該如何映射每一個迭代的變量”。那么來舉幾個例子給大家看看,Lambda 應(yīng)該怎么寫:
element => element
:將迭代過程的變量直接當(dāng)成結(jié)果反饋出來,那么就表示映射關(guān)系沒有任何變動;element => element + 1
:將迭代過程的變量增大一個單位后反饋出來,那么對應(yīng)到整個集合來說,就是將每一個元素都增大一個單位;element => element * element
:將迭代過程的變量平方后反饋出來。
比如這樣的一些情況。
另外,可以從第一個參數(shù)看出,參數(shù)類型是 IEnuemrable<T>
就意味著該擴展方法支持的實例只需要可以 foreach
就可以了,那么比如說數(shù)組、List<>
順序表、Stack<>
棧等等數(shù)據(jù)結(jié)構(gòu),因為它們都實現(xiàn)了這個接口,因此它們?nèi)伎梢允褂?Select
這個擴展方法。另外,LINQ 查詢表達式可用的前提就是實現(xiàn)這些方法,因此,這些數(shù)據(jù)類型也都直接可以使用查詢表達式。
換句話說,只要實現(xiàn)了 IEnumerable<T>
接口的所有數(shù)據(jù)類型,全部都可以使用 select
從句的查詢表達式語法。當(dāng)然,值類型也可以,只要實現(xiàn)了這個接口。只不過,因為值類型實現(xiàn)接口帶有隱藏的裝箱機制,因此值類型語法上支持,但性能上就有一些下降了。下面的圖展示了如何將一個查詢表達式轉(zhuǎn)換為一個方法調(diào)用的等價寫法。

另外,Visual Studio 的代碼提示功能也相當(dāng)有幫助。如果我們把鼠標(biāo)放在 select
關(guān)鍵字上的話,你可以立馬查看這個時候 select
調(diào)用的是什么方法,就像是這樣。

可以看到,此時我們可以查看到這個擴展方法的簽名:IEnumerable<int>.Select<int, int>(Func<int, int>)
。這確實對應(yīng)了我們這里的 Select
擴展方法的實際寫法,是科學(xué)的。Visual Studio 誠不我欺
2-2 Select(e => e)
到底有沒有用?
可以從上面的調(diào)用轉(zhuǎn)換規(guī)則看出,如果我寫了一個 collection.Select(e => e)
的過程,這就意味著我每一個元素原封不動一點都沒轉(zhuǎn)換就給返回出來了。那么,顯然在這種寫法下就沒有任何的意義。那么,它對應(yīng)的等價查詢表達式寫法呢?
這有用嗎?答案是沒有。既然是等價的,那么 Select
沒用,這個就更沒用了。因此在我們寫代碼的時候,應(yīng)該避免這一點。這一點唯一有一點用途是,不論 collection
這個變量是什么類型,通過 Select
映射過后,都是 IEnumerable<T>
的結(jié)果類型。因此,它相當(dāng)于做了一次隱式轉(zhuǎn)換的過程。
2-3 另一個重載:Select((變量, 索引) => 結(jié)果表達式)
Select
方法還有一個重載版本。這個參數(shù)的第二個參數(shù)傳入的 Lambda 要求兩個參數(shù)而不是一個了。第二個參數(shù)是固定的 int
類型,那么這個東西是拿來干嘛的呢?
可以猜到,int
類型是固定的,那么確實存在一個跟 int
類型綁定起來的概念,是索引。是的,Select
的這個重載要求傳入的 Lambda 對應(yīng)的兩個參數(shù)分別是迭代變量,以及當(dāng)前迭代過程的時候,它位于集合的第幾個元素,也就是索引。注意,這里的索引的概念仍然是從 0 開始編號的。
如何使用呢?考慮一種情況。如果我們不得不要使用迭代變量此時的索引的時候,我們可以試著使用該重載。假設(shè)我現(xiàn)在仍舊傳入這個數(shù)組,不過我在映射后需要同時反饋出對應(yīng)的索引,我們可以這么做:
我們傳入這個 Lambda 要兩個參數(shù),第二個參數(shù)就是當(dāng)前迭代變量 element
的索引。然后我們使用匿名類型來接收該結(jié)果。在 foreach
得到結(jié)果的時候,就可以使用 .Index
得到對應(yīng)的索引數(shù)值。
這個方法用得比較少,所以就點到為止,而且該方法重載因為帶有索引這個機制,所以無法等價轉(zhuǎn)換為查詢表達式寫法。
Part 3 SelectMany
方法組
下面我們來說一下 SelectMany
方法組。SelectMany
方法組是實現(xiàn)笛卡爾積的基礎(chǔ)。這個方法組的名字 SelectMany
看名字還不太理解,如果我們拆開來看的話,select 是選擇(表示映射),而 many 是“多”的意思,所以 select many 可以大膽猜測,它表示映射的是一對多的一種關(guān)系。試想一下,如果我要連續(xù)兩個 from
從句的話,外層的 from
每一次迭代得到的一個迭代變量,都會和內(nèi)層用來迭代的這個集合的每一個元素逐一完成匹配。而這種關(guān)系在 LINQ 就稱為 select many,所以,這個方法就是拿來干這個的:用于映射一對多關(guān)系。
該方法組包含 4 個重載。
不帶索引
SelectMany(集合, 迭代變量 => 映射集合)
SelectMany(集合, 迭代變量 => 映射集合, (變量1, 變量2) => 結(jié)果表達式)
帶索引
SelectMany(集合, (迭代變量, 索引) => 映射集合)
SelectMany(集合, (迭代變量, 索引) => 映射集合, (變量1, 變量2) => 結(jié)果表達式)
按照前文說過的“是否在迭代的時候需要帶有索引”可以分為兩類。第一類是不帶索引的重載,一共有兩個;第二類則是帶索引的重載,也一共有兩個。先來看不用索引的,這個比較簡單。
這四個重載不論是哪一個,結(jié)果返回的都是一個 IEnumerable<>
接口的對象,允許你繼續(xù)使用 foreach
迭代它。
3-1 SelectMany(變量 => 集合)
第一個方法最簡單,除了 this
參數(shù)外只剩下一個參數(shù),傳入一個 Lambda,帶有一個參數(shù)表示迭代過程的變量,按照 SelectMany
的中心思想,所以它映射的操作是:在迭代出整個集合的過程之中,將當(dāng)前迭代的變量和我們指定的集合進行逐個對象的映射。用法是這樣的:
假設(shè)我有一個 array
數(shù)組,我要把這個集合和另外某個集合的每一個元素挨個進行排列組合的話,可以用這個方法。注意,這個方法的 Lambda 返回一個集合,這一點有些奇怪,也不是很好理解。因為 SelectMany
是一對多的關(guān)系,所以迭代變量 element
通過這里的 Lambda 映射到一個集合上去,這才是純正的一對多。
怎么用呢?假設(shè)我想要兩個數(shù)組里各自取出一個元素相乘得到的結(jié)果,如果這些乘積要枚舉兩個集合里的每一個元素的話,這自然就是笛卡爾積的基本用法,寫查詢表達式將非常簡單:
那么,等價調(diào)用這個方法應(yīng)該怎么做呢?這就發(fā)揮了這個方法的用途了。我要想挨個進行映射,還要得到結(jié)果的話,說明我當(dāng)前這個迭代變量要匹配上一個集合,并且該集合要返回我們想要的乘積的結(jié)果。那么,這樣的乘積表達式可以這么做:
這兩個寫法都是可以的,因為它們是等價的,這一點前面已經(jīng)說過了。而我們仔細看一下內(nèi)部的這個 Lambda,挺神奇的。e1
是我們迭代 array1
序列的臨時變量,表示的意義是我期間迭代過程之中的臨時變量,也就是我們所說的迭代變量。然后 Lambda 表達式里,我們給返回表達式寫上一個新的查詢表達式,迭代 array2
變量,并將剛才的 e1
和我此時迭代的迭代變量 e2
求乘積??梢钥吹竭@個查詢表達式代表的集合,實際上是迭代 array2
變量,并且映射的結(jié)果是將每一個元素都和 e1
這個數(shù)字相乘,得到的乘積的結(jié)果。
而你自己想想看,我反復(fù)在映射得到的結(jié)果都是乘積結(jié)果,所以放眼望去,整個 array1
都迭代完成之后,是不是就相當(dāng)于是將每一個元素的匹配過程,并且相乘的結(jié)果全都迭代出來了?所以,這個寫法沒有任何的問題,這便是這個重載的用法。
3-2 SelectMany(變量 => 集合, (變量1, 變量2) => 表達式)
另外一種騷操作來實現(xiàn)剛才等價的笛卡爾積寫法的調(diào)用方法是這個重載。這個重載更有趣,它要兩個 Lambda。其中第一個 Lambda 的迭代變量可能沒啥用途,返回的集合則是我們內(nèi)層和外層逐個匹配的集合,而第二個 Lambda 則是將剛才第一個集合和前一個 Lambda 里返回的集合,進行逐個匹配。匹配的過程之中,兩個迭代變量就是這個 Lambda 的參數(shù),而返回一個集合作為映射的結(jié)果。
按照這個過程,我們可以把剛才的代碼改成這樣,來調(diào)用這個方法重載,且實現(xiàn)的結(jié)果是完全一樣的:
這種寫法和剛才的調(diào)用寫法不同,但結(jié)果是完全一樣的。請注意第一個 Lambda 參數(shù),它的參數(shù) e1
并未在返回表達式 array2
里用到,但是這是預(yù)期的,因為這個方法就這么用的。
那么,前文介紹的三種迭代模式返回的結(jié)果全部是一樣的。完整的代碼是這樣的:
實際上,因為這個實現(xiàn)機制有兩種等價的轉(zhuǎn)換關(guān)系,所以編譯器可以選擇其中任何一個都行。不過,編譯器選擇的是這個重載,對,稍微復(fù)雜一點的這個。和前面一樣,這里也給一個圖給大家展示一下等價轉(zhuǎn)換的關(guān)系:

from
從句的內(nèi)層這個 from

3-3 SelectMany((變量, 索引) => 集合)
該方法允許我們在第一個 Lambda 里添加一個額外的參數(shù),表示的是當(dāng)前迭代變量的對應(yīng)索引。如果需要的話可以用這個方法。
3-4 SelectMany((變量, 索引) => 集合, (變量1, 變量2) => 表達式)
該方法也允許我們在第一個 Lambda 里添加一個額外的參數(shù),表示的是當(dāng)前迭代變量的對應(yīng)索引。如果需要的話可以用這個方法。
Part 4 select-into
從句呢?
前文介紹了基本的 select
用法,不過還剩下一個情況沒有說到:into
。LINQ 允許使用 into
從句,表示定義一個變量,阻斷前面的查詢,以結(jié)果的形式進行繼續(xù)后續(xù)的迭代。
因為 select-into
不存在任何的副作用,因此前面的查詢結(jié)果可以完全拆解成一個單獨的查詢表達式。正是因為如此,select-into
從句可以等價轉(zhuǎn)換成兩個 Select
擴展方法的調(diào)用。
比如這樣的代碼:
可以等價轉(zhuǎn)換為這樣:
第一個 Select
方法調(diào)用得到映射關(guān)系表,而第二個 Select
方法則根據(jù)第一個映射后的序列進行逐個的迭代返回。因為查詢表達式里我們并未對 another
變量再次做任何的變換,因此轉(zhuǎn)換為等價的方法調(diào)用時,第二個 Select
方法的 Lambda 參數(shù)是 another => another
。
可以從這個地方看出,這個寫法是多余的,因為前面說過了。所以,我們也需要盡量避免出現(xiàn) into
從句后又一次迭代返回它自己的行為——它可以省略一次查詢過程:去掉 into another select another
這部分內(nèi)容。
不過,如果你真的這么寫了,也不是不行??梢园咽髽?biāo)放在兩個 select
上,就可以看到兩次 Select
調(diào)用過程:


from
和 select
的用法給大家簡要做了一個原理上的介紹。實際上,只要只有這兩個從句類型的任何組合情況,都可以轉(zhuǎn)化為上面給出的這些方法調(diào)用過程,比如說三層甚至更多層次的 from