第 102 講:C# 3 之查詢表達(dá)式(六):join、on 和 equals 關(guān)鍵字
join
系列從句。這個(gè)從句放在最后講解是因?yàn)樗铍y,用法比較多,而且在處理機(jī)制上相對(duì)比較復(fù)雜。
Part 1 引例:下弦之鬼和寵物的對(duì)應(yīng)關(guān)系
假設(shè)你看過動(dòng)漫《鬼滅之刃》。我們這里定義一種數(shù)據(jù)類型叫 Demon
,存儲(chǔ)的是鬼的名字:
這里我們使用 private set
的原因是,C# 3 不允許只有 get
的自動(dòng)屬性,因此我們補(bǔ)充了一個(gè)私有化了的 setter 防止被外界調(diào)用,也可以簡(jiǎn)化代碼避免寫全字段。
接著,我們定義六個(gè)下弦鬼以及對(duì)應(yīng)的信息:
從 lowerOne
到 lowerSix
分別對(duì)應(yīng)下弦之一到下弦之六:魘夢(mèng)、轆轤、病葉、零余子、累和釜鵺。

然后,我們假設(shè)給它們定義一些寵物。
當(dāng)然,有的鬼沒有寵物,有些鬼可以包含多個(gè)寵物,假設(shè)我們有這么一些寵物:
我們?cè)诔跏蓟臅r(shí)候都給每一個(gè)寵物配上了對(duì)應(yīng)的名字和它的主人。我們暫時(shí)不考慮寵物是貓是狗還是什么其它的動(dòng)物類型。
現(xiàn)在我們想要使用查詢表達(dá)式獲取所有寵物的對(duì)應(yīng)主人,然后構(gòu)成一個(gè)映射關(guān)系,將它們表示出來。這個(gè)怎么做呢?
最好想到的辦法就是使用交叉連接(Cross Join)。交叉連接也叫笛卡爾積(Cartesian Product),指的是兩個(gè)序列,第一個(gè)序列的每一個(gè)元素都和第二個(gè)序列的每一個(gè)元素一一配對(duì)的行為。在 LINQ 里面,則對(duì)應(yīng)的是兩個(gè)挨著的 from-in
從句。因?yàn)閮蓚€(gè)連續(xù)的 from-in
從句在翻譯成 foreach
的等價(jià)代碼的時(shí)候,是嵌套的,嵌套的循環(huán)就是外層的一個(gè)元素對(duì)應(yīng)內(nèi)層的一組元素,畢竟,內(nèi)層循環(huán)的所有元素都得遍歷完之后,外層循環(huán)才會(huì)繼續(xù)迭代下一個(gè)元素,這恰好是笛卡爾積的基本用法和定義。
我們使用 from-from-where-select
的從句序列來表示笛卡爾積和匹配。第 4 行代碼使用一次 where
從句可以篩選和計(jì)算是否當(dāng)前 pet
對(duì)象的 Owner
屬性(主人)是當(dāng)前迭代的 demon
對(duì)象。
注意中間我們用的是 ==
運(yùn)算符。因?yàn)槭且妙愋?,而我們?duì) Demon
數(shù)據(jù)類型并未涉及任何的比較操作的重載行為,所以這個(gè) ==
是在比較兩個(gè)對(duì)象的引用是否一致。它等價(jià)于在調(diào)用 ReferenceEquals
方法。顯然,我們?cè)诖a里面,確實(shí)給每一個(gè)寵物的主人傳參的都是下弦之鬼的實(shí)例引用,因此這么去比較沒有任何問題。
接著,我們最后使用 select
從句將寵物名稱和對(duì)應(yīng)主人的名稱使用匿名類型映射出來。最后得到的就是所有一一匹配的結(jié)果了。
仔細(xì)琢磨一下,這個(gè)查詢表達(dá)式是否嚴(yán)謹(jǐn),會(huì)不會(huì)多出冗余情況,也會(huì)不會(huì)漏掉情況。其實(shí)是不會(huì)的,因?yàn)閮蓚€(gè)迭代過程使用的是不同的數(shù)組對(duì)象,迭代的成員完全不沖突因此不會(huì)造成冗余或漏掉的情況,畢竟就是一一匹配的。
下面我們來調(diào)試一下,看看這樣的篩選是否成功:
我們來看一下運(yùn)行結(jié)果吧。

合理。答案是正確的。不過,答案使用的是下弦之鬼的真名,因此對(duì)于不熟悉的人來說,看著不太友好。我們給 Demon
的構(gòu)造器加上一個(gè)參數(shù),表示下弦級(jí)別;然后顯示結(jié)果的時(shí)候也是顯示它的級(jí)別;順帶加上重寫的 ToString
方法,以便顯示和輸出對(duì)應(yīng)的結(jié)果。
注意第 14 行的這個(gè)寫法。我們是直接在數(shù)組后加上的索引器。這種寫法有些時(shí)候是可以的,它表示直接在“即定義即用”的數(shù)組序列里去取值。接著,Level
的范圍是 1 到 6,而數(shù)組下標(biāo)從 0 開始,因此我們給這個(gè)“即定義即用”的數(shù)組的第一個(gè)元素配上了 null
數(shù)值占位,保證 Level
是 1 的時(shí)候,直接取序列的第 2 個(gè)元素(索引的時(shí)候?qū)懗?[1]
,而這個(gè) 1 就是 Level
的值)。
然后,同步地更改構(gòu)造器傳參:
select
從句部分,把原來的 .Name
屬性引用部分給去掉。因?yàn)閷懥?Name
接著,我們來看一下結(jié)果:

完美。我們完成了對(duì)應(yīng)關(guān)系的查詢。
不過,這樣的寫法有點(diǎn)復(fù)雜,有沒有稍微簡(jiǎn)單一點(diǎn)的寫法來完成計(jì)算?有。
Part 2 join-in-on-equals
從句
我們現(xiàn)在使用一個(gè)全新的從句類型:join-in-on-equals
從句。我們先考慮把兩個(gè)數(shù)組提取出來單獨(dú)搞成變量,以免查詢表達(dá)式過長(zhǎng):
接著,我們?cè)瓉淼恼Z句是這樣的:
from pet in pets
替換為 join pet in pets
;而 where pet.Owner == demon
替換為 on pet.Owner equals demon
是的。這就是新的從句類型:join-in-on-equals
從句??梢钥吹疥P(guān)鍵字一共是 4 個(gè),每?jī)蓚€(gè)關(guān)鍵字的中間都會(huì)插入一個(gè)表達(dá)式的數(shù)據(jù)進(jìn)去,這樣比起原來寫兩層 from-in
從句的好處就是更具有可讀性。
join-in-on-equals
從句的語法如下:
我相信你通過前文的改法可以直接看出這個(gè)寫法的神奇之處。這個(gè)寫法我們直接用 equals
關(guān)鍵字來表示了相等判斷,而 from-in-where
兩行被改成了一行。而改成這樣的語法后,equals
兩側(cè)的對(duì)象直接作為比較信息出現(xiàn),于是其中一個(gè)用的是 pet
join
從句上面的 from
從句提供的迭代變量的信息參與比較。這樣的書寫模式使得“查詢集合和集合之間的關(guān)系”的操作更具有可讀性和體系化的處理模式。
因?yàn)檫@樣的代碼從更普通的 from-from-where-select
改成了更具體系化的 from-join-select
,因此我們也給這樣的從句模式取了一個(gè)術(shù)語名詞。我們認(rèn)為,我們?cè)谥鹨黄ヅ淦陂g,篩選掉了不滿足條件的內(nèi)容,通過 equals
兩側(cè)的對(duì)象比較相等性作為基本判斷的操作,因此結(jié)果是來自于兩個(gè)列表的元素構(gòu)成的集合。比如前面的例子里,下弦之鬼有 6 個(gè),而寵物也有 6 個(gè),如果一一匹配的話,得到的總組合情況數(shù)量肯定是 36 個(gè);但因?yàn)?equals
兩側(cè)對(duì)象的比較過程的篩選,導(dǎo)致了最終的合理匹配結(jié)果不足 36 個(gè)(當(dāng)然,如果你寫的是 on true equals true
的話……這個(gè)反人類的情況就不多說了)。我們把這種邏輯上整合兩個(gè)表的數(shù)據(jù)湊成新結(jié)果的操作稱為連接(Join),而我們把邏輯上“取交集”的拼接操作稱為內(nèi)連接(Inner Join)。
至于別的連接操作,我們會(huì)在本文稍微靠后一點(diǎn)的地方介紹,因?yàn)楸容^多也比較復(fù)雜。
Part 3 join-in-on-equals-into
從句
into
關(guān)鍵字是老熟人了,在 select
和 group
里都有所使用。今天我們要看看的是 into
搭配 join
的用法。不過,它比 group-by-into
還要難理解一點(diǎn),所以很多同學(xué)初學(xué) LINQ 的時(shí)候都栽在這里了。
3-1 join
往往體現(xiàn)的是“一對(duì)多”的用法
要想理解 join
后拼接 into
的用法,我們就得回憶一下,join
用起來的意義。join
在前文是用來代替兩層連續(xù)的 from
從句的,目的就是為了讓可讀性提高一些。因?yàn)?from
和 from
的拼接看起來就很“普通”,于是不容易讓用戶思考和想象出來它的真實(shí)用法,而給出 from-join
后就會(huì)立馬知道,join
的出現(xiàn)就是為了匹配前文給出的信息列表的,于是使用范疇就會(huì)稍微變窄一點(diǎn),這樣的方式提升的可讀性。
而整個(gè) join
要用到四個(gè)關(guān)鍵字:join
、in
、on
和 equals
,每?jī)蓚€(gè)相鄰關(guān)鍵字之間都會(huì)插入信息和變量來維持語法的正確性。整個(gè) join
的用法是:
join-in
兩個(gè)關(guān)鍵字中間的變量表示迭代和匹配前面的列表的單位對(duì)象;in-on
兩個(gè)關(guān)鍵字中間的變量表示迭代的列表對(duì)象;on
和equals
兩個(gè)關(guān)鍵字中間的變量表示匹配期間的條件到底是“誰和誰相同”的第一個(gè)變量;equals
后的變量表示匹配期間的條件到底是“誰和誰相同”的第二個(gè)變量。
大概從這樣的介紹文字里看出,它就是 from-in
的翻版寫法。不過,這里 into
是直接跟在 join-in-on-equals
之后的,所以不能拿原來的思路去理解同樣的東西了。那怎么去理解呢?
試想一下,join
的目的是為了什么?目的是為了連接前文 from
的迭代序列,對(duì)吧。我要匹配正常的數(shù)據(jù),要使得某個(gè)條件上兩個(gè)對(duì)象的數(shù)據(jù)是一致的,那么它們就放在一起。那么,像是剛才的下弦鬼和寵物的對(duì)應(yīng)關(guān)系來說,鬼可以有多個(gè)寵物,所以是“一對(duì)多”的關(guān)系——一個(gè)鬼可以有多個(gè)寵物,鬼是“一”而寵物就是“多”。而正是因?yàn)樗w現(xiàn)的是“多”的這部分,所以我們把它放在了 join
這部分的迭代過程里,而沒有反過來寫(join-in
迭代 pets
,而 join-in-on-equals
則迭代的是 demons
)。
這么做是有意義的。join
體現(xiàn)的確實(shí)是一種一對(duì)多的關(guān)系,而 join
這個(gè)從句的迭代部分是作為“多”的體現(xiàn)的,而它上面緊挨著的這個(gè) from
體現(xiàn)的是“一”。這么說起來就比較好理解了:這個(gè) into
表示的是這個(gè)“一對(duì)多”過程里的“多”里的所有結(jié)果。這么說比較抽象,換到這個(gè)例子里來的話:
我如果這么寫代碼的話,那么這里的 gj
變量,表示的就是當(dāng)前的下弦鬼的所有寵物。可以看到,我們后面接了一個(gè) select
從句,直接使用匿名類型的第二個(gè)屬性 Pets
接收了這個(gè) gj
變量。那么這個(gè) selection
變量整體是啥意思呢?就是在獲取所有下弦鬼的每一個(gè)鬼的寵物信息,然后整合成匿名類型的表達(dá)形式反饋出來。
用的時(shí)候,就這么用就可以了:
首先我們來迭代 selection
變量,每一個(gè)變量都是一個(gè)匿名類型的實(shí)例。接著,我們要取出下弦鬼的名字,然后和它的寵物。這里我們要特別注意一點(diǎn):這里的“一對(duì)多”是概念上的理解方式,在實(shí)際體現(xiàn)里,可能有下弦鬼沒有任何的寵物與之對(duì)應(yīng),這是存在的。正是因?yàn)橛羞@種情況,我們才需要判斷一下。這里我們要用到一個(gè)擴(kuò)展方法:Any
。
Any
擴(kuò)展方法是顯式無參的(Explicitly Parameterless),即該方法是滿足擴(kuò)展方法的一切規(guī)則,而它實(shí)際上也只包含一個(gè)this
修飾過的參數(shù),而它因?yàn)閿U(kuò)展方法的語法被我們進(jìn)行實(shí)例前置了,所以在按照擴(kuò)展方法的語法規(guī)則書寫之后,這個(gè)參數(shù)就沒了,于是變?yōu)榱艘粚?duì)空的小括號(hào)。
我們直接跟在 currentPets
變量之后,表示的是這個(gè)集合到底有沒有元素。如果有元素,那么就返回 true
;否則返回 false
。這一點(diǎn)和普通集合的 Count
或 Length
屬性 != 0
邏輯是一樣的。只不過這里 currentPets
的實(shí)際類型不允許我們這么做——它實(shí)際上是 IEnumerable<Pet>
類型的。是的,join
后用的 into
從句,這個(gè)變量都應(yīng)為 IEnumerable<T>
的類型,而這里的 T
會(huì)根據(jù)你的 join
迭代的類型以及判斷的操作進(jìn)行調(diào)整。
3-2 分組連接的概念
現(xiàn)在我們大概能知道用 into
在 join
之后是啥意思了,但是,這個(gè)變量名就很奇怪:gj
?
實(shí)際上,在 LINQ 里,join
后跟 into
的語法被官方稱為分組連接(Group Join)。分組連接這個(gè)名字乍一看其實(shí)是不容易理解的,因?yàn)檫B接還帶分組就很離譜。但實(shí)際上,仔細(xì)思考和分析一下前面的例子就可以發(fā)現(xiàn),它確實(shí)是在連接的操作期間使用了“分組”的操作。只不過,這里的分組和上一講的分組語法 group
從概念上完全不同:這里說的“分組”,單純是指代和說明“一對(duì)多”關(guān)系里的“多”有很多,所以將對(duì)象的對(duì)應(yīng)“多”的結(jié)果全部整合到 into
后跟的這個(gè)變量里去。而思考一下實(shí)際上,這個(gè)操作也確實(shí)可以叫分組了,畢竟完整完成查詢操作之后,所有的“一對(duì)多”關(guān)系都體現(xiàn)出來了,而每一個(gè)對(duì)象都有若干對(duì)應(yīng)匹配的成員,這就是分組的思路和思維方式。
分組連接的英文 group join 的首字母是 g 和 j,所以這個(gè)變量名才叫的是 gj
。實(shí)際上,在微軟提供的 LINQ 語法教學(xué)里,gj
這個(gè)“約定俗成”的變量名也廣泛存在。比如這樣

還有這樣

兩個(gè)圖片均出自于的代碼:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/join-clause
雖然不一定都看得懂別人的代碼吧,但是你可以發(fā)現(xiàn),gj
也都“不謀而合”地出現(xiàn)在 join
從句的 into
關(guān)鍵字之后。
Part 4 說說細(xì)節(jié)
join
雖然好用,看懂了基本上就可以上手用了,但是它還是有很多細(xì)節(jié)需要我們說明清楚的。下面我們都來看看。
4-1 equals
關(guān)鍵字比較的細(xì)節(jié)
equals
左右兩側(cè)都會(huì)跟上一個(gè)變量,它表示的是什么樣子相同的條件的兩個(gè)對(duì)象才能匹配關(guān)聯(lián)起來。而這個(gè)是一個(gè)關(guān)鍵字,因此導(dǎo)致用起來仍然不夠靈活,畢竟你不能使用比如運(yùn)算符啊、比如方法調(diào)用之類的操作來判斷比較條件。
不過,這么限制是有意義的。在查詢操作期間,百分之八九十都是在進(jìn)行等值連接(Equivalent Join),即進(jìn)行 join
連接期間,用的是相等性比較,所以設(shè)計(jì)為關(guān)鍵字也是不必深究的點(diǎn)——畢竟大多數(shù)時(shí)候都是等值連接。
話說回來,那么 equals
到底是啥樣的判斷規(guī)則和過程呢?實(shí)際上,很簡(jiǎn)單。就是咱們之前學(xué)習(xí)的 EqualityComparer<>
的判斷模式。equals
會(huì)把代碼翻譯成使用類型默認(rèn)的判斷比較規(guī)則來進(jìn)行比較的過程。就是什么檢查類型的 Equals
方法和 GetHashCode
方法的重寫啊,先調(diào)用誰后調(diào)用誰啊,這些。
所以,你的代碼要想?yún)⑴c equals
關(guān)鍵字的比較操作,至少得保證對(duì)象實(shí)體包含 Equals
方法和 GetHashCode
方法的重寫。
4-2 join
里的 into
不會(huì)像 select
和 group
里那樣阻斷查詢
這一點(diǎn)比較特殊。因?yàn)樵谠O(shè)計(jì)分組連接的語法的時(shí)候,因?yàn)?LINQ 的關(guān)鍵字太多了,所以官方也不打算搞得太復(fù)雜,所以還是用了 into
來表示分組連接的結(jié)果。不過,這里的 into
不會(huì)阻斷查詢,因此你仍然可以使用前面的變量。
再來看之前這個(gè)例子就可以發(fā)現(xiàn),我們?cè)?select
里仍然使用到了 into
以前出現(xiàn)的 demon
變量。這一點(diǎn)希望你記清楚。
Part 5 靈活使用 join
從句
這個(gè)術(shù)語詞不是 LINQ 發(fā)明的,在計(jì)算機(jī)學(xué)科的關(guān)系代數(shù)(Relation Algebra)分支上,按照完成的分類,連接操作有這樣的一些情況:
按連接的機(jī)制分類:
左外連接不含內(nèi)連接(左減右):使用左外連接取所有結(jié)果的同時(shí),去掉都包含的情況;
右外連接不含內(nèi)連接(右減左):使用右外連接取所有結(jié)果的同時(shí),去掉都包含的情況;
全外連接不含內(nèi)連接(對(duì)稱差):使用全外連接取所有情況的同時(shí),去掉都包含的情況。
左外連接(也叫左連接,Left Outer Join):將左邊的列表當(dāng)成基準(zhǔn),右邊列表的元素往左邊列表上拼接(拼接在左邊列表的右側(cè))。如果沒有的部分字段會(huì)保持
null
數(shù)值;右外連接(也叫右連接,Right Outer Join):將右邊的列表當(dāng)成基準(zhǔn),左邊列表的元素往右邊列表上拼接(拼接在右邊列表的左側(cè))。如果沒有的部分字段會(huì)保持
null
數(shù)值;全外連接(也叫全連接,F(xiàn)ull Outer Join):不按具體那個(gè)列表當(dāng)基準(zhǔn),而是取出兩個(gè)列表的全部情況,左邊列表沒有的部分補(bǔ)充
null
數(shù)值,而右邊列表沒有的部分補(bǔ)充null
數(shù)值,并湊成一個(gè)列表。外連接(Outer Join)
內(nèi)連接(Inner Join):基本連接手段。只有兩個(gè)列表都包含的數(shù)據(jù)才拼接起來,如果只有一個(gè)列表里有這個(gè)數(shù)據(jù),而另外一個(gè)表沒有,這樣的數(shù)據(jù)會(huì)被忽略掉,以至最終結(jié)果里沒有它的出現(xiàn)。
排除連接(Excluding Join):將前面的連接操作混合使用的復(fù)雜連接方式。
按連接的條件分類:
等值連接(Equivalent Join,簡(jiǎn)稱 Equijoin):將連接操作期間使用的條件(比如內(nèi)連接的拼接依據(jù))設(shè)定為相等性判斷的連接操作;
非等值連接(Inequivalent Join,簡(jiǎn)稱 Non-equijoin):將連接操作期間使用的條件(比如內(nèi)連接的拼接依據(jù))設(shè)定為不等性判斷的連接操作;
自然連接(Natural Join):將連接操作期間使用的條件設(shè)定為相等性判斷的連接操作,并在連接之后直接刪除掉重復(fù)的列;
交叉連接(Cross Join):就是笛卡爾積,沒有任何條件,直接兩兩直接拼接即可;
組合鍵連接(Join by Composite Key):使用復(fù)雜的對(duì)象作為條件判斷依據(jù)的連接。
LINQ 的特有連接機(jī)制:
分組連接(Group Join):將連接的操作結(jié)果臨時(shí)定義為一個(gè)單獨(dú)的組用于后續(xù)操作。
我知道,是挺復(fù)雜的,所以我并不打算全都說明,畢竟 LINQ 的 join
也確實(shí)不能做到前面提到的這些全都的連接操作,而且內(nèi)連接、分組連接以及等值連接已經(jīng)在前面說過了。
5-1 DefaultIfEmpty
和 First
擴(kuò)展方法
為了銜接后面的內(nèi)容,我們需要優(yōu)先介紹一個(gè) .NET 自帶的方法操作:DefaultIfEmpty
擴(kuò)展方法。這個(gè)方法比較繞。我還是舉例說明一下。
這個(gè)調(diào)用操作表示,如果 pets
集合序列沒有包含任何元素(即 Any
方法調(diào)用后返回 false
),那么這個(gè)方法調(diào)用將會(huì)返回一個(gè)包含 default(Pet)
的元素的集合;否則,pets
里包含元素,于是就把它自己返回出來。
這個(gè)做法有什么意義呢?DefaultIfEmpty
被設(shè)計(jì)出來主要是為了提供默認(rèn)集合。所謂的提供默認(rèn)集合,主要的地方用在哪里呢?考慮一種情況。假設(shè)我們有這樣的操作,在已經(jīng)篩選的集合里進(jìn)行匹配。
First
可問題是,這個(gè)方法有個(gè)問題在于,如果序列集合沒有元素的話,就會(huì)拋出異常,畢竟沒有元素為啥還要取元素呢?于是,我們需要手動(dòng)判斷:
現(xiàn)在,這個(gè)方法從另外一個(gè)角度允許了我們少使用復(fù)雜的處理過程:
DefaultIfEmpty
方法表示,如果序列沒有元素,那么就會(huì)默認(rèn)返回一個(gè)集合,這個(gè)集合只包含一個(gè)元素,這個(gè)元素的值就是這個(gè)集合每一個(gè)元素應(yīng)該的數(shù)據(jù)類型 T
的默認(rèn)數(shù)值 default(T)
。有點(diǎn)繞,簡(jiǎn)單來說就是,序列的每一個(gè)元素是 Demon
類型的,而 DefaultIfEmpty
方法調(diào)用的時(shí)候,如果序列沒有元素,那么就會(huì)默認(rèn)產(chǎn)生一個(gè)集合進(jìn)行返回,該集合只有一個(gè)元素,數(shù)值是 default(Demon)
,也就是 null
。于是,我們可以針對(duì)于這個(gè)集合繼續(xù)進(jìn)行 First
方法的調(diào)用,這樣就避免了異常拋出。而且這個(gè)辦法巧妙的是,我們省去了條件運(yùn)算符(或者 if
語句)的使用;null
直接通過 DefaultIfEmpty
的默認(rèn)情況直接體現(xiàn)出來了,于是不用我們手寫出來。
當(dāng)然了,如果你要自定義默認(rèn)返回值的話(即想改變返回默認(rèn)的情況是 null
還是別的什么的時(shí)候),你也可以給這個(gè) DefaultIfEmpty
方法上額外傳入一個(gè)參數(shù),調(diào)用它的重載版本:
這里我們定義了一個(gè)默認(rèn)對(duì)象,它表示如果集合里沒有元素的時(shí)候會(huì)默認(rèn)返回一個(gè)帶有這個(gè)對(duì)象的集合。它等價(jià)于
明白了嗎?
要知道,join
也并非只能放在 select
之前緊挨著的地方。C# 的 LINQ 十分靈活,正是因?yàn)樗撵`活度,所以上面的連接操作才會(huì)有文章可以寫。
前面我們講的是內(nèi)連接,內(nèi)連接不會(huì)產(chǎn)生額外的信息,畢竟是內(nèi)部相同比較匹配。但是,如果要想去做到外連接(Outer Join)的話,就比較麻煩了。
我們先要說的是外連接的基本概念。外連接是讓一個(gè)列表作為基本列表,然后讓另外一個(gè)列表去往上匹配。這個(gè)作為基本列表的表按照一定的條件和另外一個(gè)列表的元素進(jìn)行匹配,如果找到了的話,就連接起來。但是,如果基本列表里有,但往上拼接的表里沒有的元素的話,就會(huì)被補(bǔ)上默認(rèn)數(shù)值。
外連接和內(nèi)連接的區(qū)別就在于這一點(diǎn)。外連接有基本列表,這會(huì)使得拼接和連接的操作更加嚴(yán)謹(jǐn)和細(xì)節(jié);而內(nèi)連接因?yàn)椴恍枰紤]連不上的情況,因此會(huì)更實(shí)用。
舉個(gè)例子吧。假設(shè)我現(xiàn)在有兩個(gè)列表,一個(gè)是下弦之鬼,一個(gè)則是寵物。我們要讓寵物的主人作為拼接條件,和下弦之鬼逐個(gè)進(jìn)行判斷和拼接。不過,有些下弦之鬼是沒有寵物的,因此這個(gè)時(shí)候在前文的例子里,我們并不能體現(xiàn)出它們。如果要體現(xiàn)出它們的話,我們需要使用左外連接來完成這個(gè)任務(wù)。
左外連接就是基于左列表的外連接,即將操作的兩個(gè)列表里的左邊這個(gè)作為基本列表?,F(xiàn)在我們要取出這種對(duì)應(yīng)關(guān)系的時(shí)候,有些下弦之鬼是沒有寵物的,但是我們?nèi)耘f需要體現(xiàn)出來,因此“下弦之鬼”這個(gè)列表就得作為基本列表;而“寵物”則是附加的表。來看看怎么做。
請(qǐng)看示例。這個(gè)例子里,我們改變了一下代碼的執(zhí)行邏輯,雖然我們?nèi)匀皇褂昧朔纸M連接,但這個(gè)時(shí)候我們?cè)?join
后又一次跟了一個(gè) from
從句,目的是為了匹配和顯示我們這里的左外連接的運(yùn)算規(guī)則。
我們?cè)?from subpet in gj.DefaultIfEmpty
這個(gè)部分里用到了 DefaultIfEmpty
方法的調(diào)用,這里就可以看到 DefaultIfEmpty
的作用了:DefaultIfEmpty
即使里面沒有元素,也會(huì)產(chǎn)生一個(gè)默認(rèn)元素,不影響迭代。
然后我們把取出的對(duì)應(yīng)關(guān)系序列,按照 select
從句進(jìn)行映射。注意這里的第二個(gè)屬性是 Pet
了,因?yàn)檫@里我們是迭代了 gj
的序列,因此不再是整個(gè)返回。那么,怎么用這個(gè)變量呢?
這樣就可以了。這就是左外連接。我們將“左列表”(下弦之鬼)作為基本表,永遠(yuǎn)顯示里面的成員;而“右列表”(寵物)作為附加規(guī)則給附加上去。如果說右列表里沒有對(duì)應(yīng)的情況的話,我們將會(huì)輸出默認(rèn)數(shù)值(對(duì)應(yīng)了這里的第 6 行輸出代碼)。這就是左外連接。
那么右外連接呢?很遺憾,C# 的 LINQ 不能實(shí)現(xiàn)右外連接。本節(jié)結(jié)束。
雖然我很想說這是開玩笑這是調(diào)侃,但確實(shí)如此。C# 的 LINQ 并不能做到和模擬右外連接。你只能將表格反過來使,交換了左右列表后,再使用左外連接的方式來模擬出右外連接的運(yùn)算。實(shí)際上,通過上面的例子也看得出來,這種連接操作也就左外連接比較好用,也比較實(shí)用。
5-3 非等值連接
由于 join
里用 equals
關(guān)鍵字是固定行為,所以你無法使用 join
來達(dá)成除了相等匹配以外的所有情況。不過,這也不代表我們做不到。由于 join
是 from
改過來的,所以我們?nèi)匀豢梢允褂玫芽柗e來完成這一點(diǎn)。
假如我有一系列的商品,每一個(gè)商品都貼有標(biāo)簽標(biāo)識(shí)它是啥類型的商品。我現(xiàn)在要想找到合適的商品,這些商品的標(biāo)簽必須在我現(xiàn)在給定的標(biāo)簽序列里包含有的情況。這怎么找呢?
這要是寫成 join
的話,由于條件沒辦法使用 Contains
這樣的情況,因此無法做到。
5-4 表達(dá)式為 join
連接條件的連接
假設(shè)我們?nèi)匀皇褂眠@個(gè)例子來舉例,只不過這次我們不直接匹配名稱了,而是去模擬匹配。假設(shè)像是張三李四王五趙六這樣,我們只要求姓氏相同就可以連起來的話,join
就會(huì)這樣做:
這里的 Split
方法是將字符串按指定字符進(jìn)行切割。遇到這個(gè)字符的時(shí)候,就拆開字符串的左右兩側(cè),以這個(gè)字符為分界點(diǎn)。那么 demon.Split(' ')
和 pet.Split(' ')
的作用就是將單詞按空格分割,于是得到的結(jié)果就自然是姓名了。我們?nèi)〕龅谝粋€(gè)部分,自然就是這個(gè)人的姓。
特別注意,由于 equals
兩側(cè)的數(shù)據(jù)是 string
類型,而 string
實(shí)現(xiàn)了比較相等的規(guī)則,因此這樣的寫法是允許的。
5-5 組合鍵連接
LINQ 還能進(jìn)行組合鍵連接。上面我們講了一下如何使用表達(dá)式進(jìn)行相等性比較,甚至是……組合鍵(Composite Key)。
所謂的組合鍵,在 LINQ 里你可以理解成 equals
兩側(cè)的對(duì)象為匿名類型的情況。
好比我現(xiàn)在有這么一個(gè)序列。這個(gè)序列里包含眾多的用戶點(diǎn)單信息(Orders
屬性)。現(xiàn)在,我要獲取的是商品和用戶點(diǎn)單之間的這么一個(gè)關(guān)系的話,我們就需要使用組合鍵連接。
這里,我們將 join
從句的 equals
判斷部分寫成匿名類型的相等性比較,標(biāo)識(shí)我除了商品要一致以外,點(diǎn)單的單號(hào)也要一致,于是就有了上面展示的專業(yè)的代碼的判斷過程。
至此,我們就將 join
的相關(guān)用法都給大家全部介紹了一遍。LINQ 的關(guān)鍵字就算是講完了。下一講我們不打算講解內(nèi)容,先給大家布置一些有關(guān) LINQ 語法可以做到的查詢操作作為練習(xí),給大家練習(xí)練習(xí)。