GroupJoin的原碼探索

今天要來看GroupJoin的內部實作,知道GroupJoin的使用方式後,應該不難猜出它的實作可能跟Join很相似: 因為GroupJoin主要還是做Join的動作,只是最後加了個resultSeletor讓它可以對每個鍵值做彙整,現在來看看是不是真的是這樣吧。

原始碼分析

Source Code: GroupJoin.cs

如往常一樣,我們來看一下GroupJoin的公開方法,再一次的如同之前Join的原始碼,這裡只做了兩件事:

  • 檢查傳入參數是否為空,如果是空的拋出ArgumentNull例外
  • 所有參數皆合法的話叫用GroupJoinIterator取得Iterator之後回傳

其實在System.Linq專案中的公開方法都是這樣做處理的,只有做檢查參數的動作,接著就丟給其他Method做事,以後如果沒有其他特別的處理,都會像上面一樣用文字說明,就不再貼原始碼了。

接著我們來看一下GroupJoinIterator的實作:

private static IEnumerable<TResult> GroupJoinIterator<TOuter, TInner, TKey, TResult>(
    IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, IEnumerable<TInner>, TResult> resultSelector,
    IEqualityComparer<TKey> comparer)
{
    using (IEnumerator<TOuter> e = outer.GetEnumerator())
    {
        if (e.MoveNext())
        {
            Lookup<TKey, TInner> lookup = Lookup<TKey, TInner>.CreateForJoin(inner, innerKeySelector, comparer);
            do
            {
                TOuter item = e.Current;
                yield return resultSelector(item, lookup[outerKeySelector(item)]);
            }
            while (e.MoveNext());
        }
    }
}

這邊可以看到跟JoinIterator很像的實作,差別在於下面兩點:

  • 未檢查inner是否有值就叫傳入resultSelector作結果輸出
  • 沒有對inner再做一次迴圈,而是直接把整包inner丟給resultSelector

第一點是GroupJoinLeft Join的原因,在沒有inner資料的情況下還是會執行yield return,就算只有outer的資料也可以回傳。

第二點是GroupJoin可以彙整資料的原因,因為沒有做迴圈,所以丟給resultSelector的是inner集合資料,所以可以用集合資料來做彙整

測試案例賞析

OuterInnerBothSingleNullElement

[Fact]
public void OuterInnerBothSingleNullElement()
{
    string[] outer = new string[] { null };
    string[] inner = new string[] { null };
    string[] expected = new string[] { null };

    Assert.Equal(expected, outer.GroupJoin(inner, e => e, e => e, (x, y) => x, EqualityComparer<string>.Default));
    Assert.Empty(outer.Join(inner, e => e, e => e, (x, y) => x, EqualityComparer<string>.Default));
}

這裡我們可以看到innerouter是有一個null的字串陣列,可是他還是會回傳有一個null的字串陣列,可是Join就會傳回空。

SelectorsReturnNull

[Fact]
public void SelectorsReturnNull()
{
    CustomerRec[] outer = new []
    {
        new CustomerRec{ name = "Tim", custID = null },
        new CustomerRec{ name = "Bob", custID = null }
    };
    OrderRec[] inner = new []
    {
        new OrderRec{ orderID = 97865, custID = null, total = 25 },
        new OrderRec{ orderID = 34390, custID = null, total = 19 }
    };
    JoinRec[] expected = new []
    {
        new JoinRec{ name = "Tim", orderID = new int?[]{ }, total = new int?[]{ } },
        new JoinRec{ name = "Bob", orderID = new int?[]{ }, total = new int?[]{ } }
    };

    Assert.Equal(expected, outer.GroupJoin(inner, e => e.custID, e => e.custID, createJoinRec));
}

在鍵值為null的情形下會拿不到inner的資料,可是outer的資料依然可以輸出。

結語

這次賞析的GroupJoinJoin大同小異,最大的差別就在於GroupJoin可以不用inner的資料也可以輸出,這也是造成GroupJoinLeft Join的原因,另外也因為GroupJoin沒有再做迴圈巡覽inner就輸出給resultSeletor做處理,所以可以直接彙整每個鍵值的資料,跟Join在用途及情境上就有差異了。

參考