LINQ Except()使用時的注意事項

最近再使用Except()時,遇到個問題。明明他的功能就是要幫我們找出a有b沒有的資料,但結果卻一直跑出a已經有了,b也有的重複資料,研究了一下發現問題還蠻有趣的,也可以順便回憶一下reference type跟value type的相關知識喔~

LINQ Except()用途主要用再取兩物件之間的差集,使用方法可以先參考下面的例子。回傳的結果為numbers1有但是numbers2沒有的陣列元素。

知道的Except()的用法後,於是我有了個想法。假設我有兩個物件,分別為group_1跟group_2。

我如果想要找出group_1有,但是group_2沒有的物件時。可能就要透過LEFT JOIN的方式來實現,SQL語法如下:

SELECT group_1.* FROM group_1
LEFT JOIN group_2 ON group_1.id = group_2.id AND group_1.name = group_2.name
WHERE b.id IS NULL

但再程式裡要透過Lambda來實現LEFT JOIN的作法又更麻煩了,程式語法如下:

            var leftJoin =
                group_1.GroupJoin(
                group_2,
                g1 => new {g1.id, g1.name},
                g2 => new { g2.id, g2.name },
                (g1, g2) => new { g1, g2 })
                .SelectMany(c => c.g2.DefaultIfEmpty(), (a, b) => new
                {
                    Id = a.g1.id,
                    Name = a.g1.name,
                    ExistInG1 = b == null ? false : true
                })
                .Where(c => c.ExistInG1 == false)
                .Select(c => c)
                .ToList();

看看上面的程式碼,要完成LEFT JOIN的功能要寫一堆又臭又長的程式。現在知道了Except的用法,是不是就可以不用透過Lambda的複雜語法完成LEFT JOIN,直接使用Except這個擴充方法來找出呢?

var except = group_1.Except(group_2);

出來的結果跟我想像的居然不一樣,依照我的理解應該是只會出現{id:2, name:Joe}這筆資料的,結果{id:1, name:Tom}這筆group_1已經有的資料居然又出現了。


爬了一下文(LINQ Except() 比對自訂類別)才發現,原來Except他的底層是使用Equals()的結果來當比對依據。也就是說,雖然group_1的{id:1, name:Tom}跟group_2的{id:1, name:Tom},這兩個物件雖然內容長的都是一樣的,但實際上在記憶體裡面,分別是屬於不同的參考位置,所以比對的結果會把這兩個物件判定為不同的物件,於是group_1的{id:1, name:Tom}又被撈取出來了,我們可以做個簡單的測試來看看,可以看到group_1[0]跟group_2[0]確實是不一樣的記憶體位置。

但如果把程式改成下面這樣的話,就可以排除掉重複資料了,merged先AddRange(group_1)後,group_1裡面的兩個參考型別物件跟merged物件AddRange後目前有的兩個參考型別物件的參考位置就會是一樣的了。這樣子Except()後的結果就不會出現重複的資料了。

            var group_1 = new List<User>()
            {
                new User() {id = 1, name = "Tom"},
                new User() {id = 2, name = "Joe"}
            };

            var merged = new List<User>();
            merged.AddRange(group_1);
            merged.Add(new User() { id = 3, name = "Joe" });

            var except =  merged.Except(group_1);

            var compare = group_1[0].Equals(merged[0]);

 

雖然是有其他比較正統的解法可以解決這個問題,但是要自己重新實作底層的介面方法,這裡就不多作介紹啦,可以自己去LINQ Except() 比對自訂類別這篇文章看看喔~

 

Ref:
1.Enumerable.Except 方法
2.LINQ Except() 比對自訂類別
3.參考資料型別(Reference Types) => 想知道陣列是參考型別或實質型別請看這篇