第 111 講:C# 3 之查詢表達式(十五):join-in-on-equals 的底層原理
今天我們來說說最后一個 LINQ 查詢表達式類型的對應底層原理:join
。
join
是一種相當奇怪和復雜的處理語句。老實說如果我們一般只用 from
、let
、where
、select
這些基本上就可以做得到和 join
一致的結果了,但是 join
仍然有一些不可替代的地方。下面我們來看看 join
的底層。
Part 1 Join
方法組:實現(xiàn) join
從句的核心
首先我們要說的是 Join
方法的系列方法組了。
1-1 Join(序列, 原序列映射, 拼接序列映射, 最終的映射表達式)
還記得我們之前的舉例嗎?我們之前用的是下弦鬼和寵物來舉例的。首先,下弦鬼是一個列表,而寵物又是一個列表。不過,寵物列表里包含的信息里是包含下弦鬼的,是因為每一個寵物都有唯一的下弦鬼與之對應(當然寵物也可能沒有被領回家)。
我們使用的就是 join
。為了成功連接兩個列表,我們使用了 join
-in
-on
-equals
這四個關鍵字組合在一起的、迄今最復雜的從句類型。
我們通過這樣的篩選就得到了結果。
那么,它對應什么方法的執(zhí)行呢?我們把鼠標放在 join
關鍵字(或者 in
、on
、equals
關鍵字都行)上面,可以看到這樣的結果:

呃,這也太長了點,甚至還是四個泛型參數(shù)(噴血.gif
不過老實說,之前的 Group
方法也達到了這個級別,只是說我們平時因為使用慣用上了 C# 提供的泛型方法的類型推斷機制,所以泛型參數(shù)甚至并不需要我們手寫。這個方法也是一樣。
我們抽取出主要成分,給大家看看這個方法究竟長啥樣。
是的,四個參數(shù)。首先我們要知道,我們需要把那兩個列表給連接起來,那么對于這個例子里,我們用到的是 demons
和 pets
,那么我們連接的操作是 pet
追加到 demon
上。因此,我們把 demons
變量放在 Join
方法的前面當實例調用,而 pets
則當成第一個參數(shù)。這就表示連接。該方法的用法也是如此:我們要把 B 往 A 上去拼接、連接的話,我們是調用成 A.Join(B, ...)
的方式的。
接著,后面三個參數(shù)全部是 lambda 表達式。第二個參數(shù)很奇怪對吧,demon => demon
好像并沒有映射改變什么啊。實際上,該參數(shù)的意思是,需要我們將寫在 Join
方法前面的實例的每一項,經過什么形式進行轉換得到的結果,然后才參與連接。顯然,我們連接寵物是想要把寵物的主人名字和下弦鬼的名字匹配上,而下弦鬼在這個連接操作里并不會發(fā)生變動,因此 demon => demon
的意思就出來了:我們只希望映射期間迭代的元素直接作為連接的實例來參與相等性比較。
第三個參數(shù)是 pet => pet.Owner
。這個就不用多說了。這個參數(shù)的意思是,將每一個寫在第一個參數(shù)上的這個列表(pets
)的每一個實例,都只取出 Owner
屬性參與相等性判斷。這一點和第二個參數(shù)的意思是類似的。
最后一個參數(shù)仍然是一個 lambda,不過這個 lambda 需要兩個參數(shù),分別對應了兩個列表的每一個實例。我們這里取出 demon
和 pet
形式化表達一下,表示我期間參與循環(huán)的迭代變量。然后我們想要的是,我們在 join
之后,應如何映射一下結果。我們知道,join
從句是不能結尾的,因此它需要后面繼續(xù)跟著一個 select
之類的從句來表達映射表達式。而這個 select
從句下的表達式的映射關系,就體現(xiàn)在我們翻譯成 Join
方法的時候的這個參數(shù)上了。比如我們剛開始映射的語句是返回一個匿名類型的實例,然后把寵物和主人的信息給放進去,那么我們在使用 lambda 的時候,就對應如此即可。
這就是 Join
的用法,以及完整的對應關系了。
1-2 Join
還允許多傳一個比較器對象
像是前文我們給出的 Demon
類型的實例,在我們使用 equals
來進行左右實例的比較的時候,我們大家都知道,等值連接是在查詢表達式里無法定義出比較規(guī)則的。
按照查詢表達式在我們早期介紹的信息里可以看出,相等判斷用的是底層一個叫 EqualityComparer<T>
的泛型類型的實例來達到的。這個泛型類型 T
的實例會智能化判斷你的這個類型 T
是否支持 Equals
和 GetHashCode
方法,以及如何實現(xiàn)相等性比較。正是因為這個原因,編譯器才敢一勞永逸針對我們完全沒有自己實現(xiàn)比較操作的實例來完成一種默認的、稍顯合理的比較規(guī)則。
而如果我們需要手動規(guī)定比較規(guī)則,而又不去影響原來的數(shù)據(jù)類型的話,我們可以考慮使用相等性的比較器。那么我們的做法和之前介紹 IEqualityComparer<T>
的實現(xiàn)規(guī)則和使用方式完全相同,實現(xiàn)一個類型,從這個接口類型派生,然后實例化這個實例類型,當參數(shù)傳入進去就可以了。
對于 Join
方法,我們也是允許的。我們只需要將這個實例類型的方法寫到前文介紹的最后一個參數(shù)之后就可以了。是的,這就是 Join
方法的第二個重載方法:允許多一個參數(shù)表示相等性比較器的對象。
Part 2 GroupJoin
方法組:實現(xiàn) join
-into
從句的核心
為了能夠繼續(xù)介紹 join
后面跟的 into
從句,我們之前引入了一個所謂的“分組連接”的概念。
看名字就看得出來:group 是分組的意思,join 在編程里是連接的意思,所以 group join 就是分組連接。我想,英語初心者可能會這么想,但如果學過專業(yè)知識甚至是專業(yè)英語的同學可能不會這么想,覺得太簡單了。
答案是這樣嗎?分組連接的英語就是 grouped join。沒有你們想象出來的那么奇怪和復雜。只不過,group 在這里是動詞,而 join 也是。直接拿來放一起肯定不行,所以 group 要變個形。
當然,這里不是給大家扯英語的環(huán)節(jié)。下面我們來看看這個方法的用法。
先來回憶一下,分組連接的邏輯,我們之前用 into
的時候都怎么用的,為啥這么用,后面又跟什么東西匹配,然后怎么構成完整的查詢表達式。
分組查詢的核心是利用了 join
從句(或者我們剛才說到的等價的 Join
方法組)的“一對多”的關系。這個一對多在我們之前也有提到過。不過,在連接的操作里,一對多的關系對照起來表示的是,from
從句的是“一”,join
迭代的對象是“多”。我們拿之前的例子舉例:
在這里例子里,我們用到了一次 into
。這個 into
表示的是,我在通過 join
和 from
的結合后,按照 equals
進行等值連接,得到的匹配項。不過,這里的 gj
變量是什么類型的呢?IEnumerable<Pet>
。還記得嗎?
這個 gj
為啥是這個類型的呢?因為我們從 into
斷開,左邊的是拼接操作,按照主人的實例來比較相等性。得到的結果,是按照連接拼接上去的,所以它會把所有的寵物的信息逐個匹配一次。我們知道這個例子里,一個寵物肯定只能擁有一個主人(或者還沒著家),但肯定不會有兩個及以上的主人。但是,一個下弦鬼肯定是可以一次性擁有多個寵物的,在這種關系上我們就是俗稱的“一對多”。
而通過這樣的規(guī)則得到了結果之后,gj
就得到的是,當前 demon
變量匹配的所有的寵物信息,所以是 IEnumerable<Pet>
類型。那么回到這個例子。我們思考一下。這個例子下,我們怎么寫成方法的調用呢?

對照這個圖片,我們可以得到 GroupJoin
方法的調用。哦豁,這個方法更復雜……不過好在,這個方法也只有一個帶 IEqualityComparer<>
接口當參數(shù)的額外重載版本。
我們按照這個展示的圖片展開查詢表達式的寫法,我們可以得到最終的結果:
對的。它和 Join
唯一的區(qū)別只差在最后一個 lambda 的第二個參數(shù)上。原本的參數(shù)是 pet
,現(xiàn)在因為得到了一對多的結果之后,這里的變量就從原來的 pet
改成了 gj
了。
用法也和之前說過的迭代方式一樣。那么我們這個就說完了。另外一個重載可以參照 Part 1 的用法帶上去就可以了,也沒有啥特別大的區(qū)別。
Part 3 總結
是的,至此我們就把查詢表達式的基本用法以及編譯器的實現(xiàn)原理給大家說明白了。實際上可以看出,實現(xiàn)原理也就是把關鍵字按照一定的規(guī)則改造成了方法調用罷了,也沒有什么好稀奇的。但是,這種改造使得我們更習慣使用關鍵字來書寫查詢表達式,因為方便還好用,這就是 LINQ 體系里非常有趣的一點。
下一講我們將給大家找一些練習題給大家做,作為階段練習。