簡體   English   中英

需要幫助,以了解使用LINQ Join和HashSet的意外行為<T>

[英]Need help understanding unexpected behavior using LINQ Join with HashSet<T>

使用C#HastSet和我不理解的LINQ的Join方法時,遇到了一些奇怪的行為。 我簡化了我的工作,以幫助專注於自己所看到的行為。

我有以下幾點:

 private HashSet<MyClass> _mySet; // module level

 IEnumerable<ISearchKey> searchKeys; // parameter.
 // Partial key searches are allowed.

 private IEqualityComparer<ICoreKey> _coreKeyComparer; // Module level.
 // Compares instances of MyClass and ISearchKey to determine 
 // if they match.

鑒於

  1. searchKeys和_mySet之間存在一對多關系。
  2. MyClass實現接口IPartialKey和ICoreKey。
  3. ISearchKey繼承自IPartialKey和ICoreKey。
  4. MyClass和ISearchKey實例均覆蓋GetHashCode方法。
  5. MyClass的哈希碼值基於其完整鍵值,包括其ICoreKey和IPartialKey值以及其他字段。
  6. MyClass使用的完整鍵不是唯一的。 兩個不同的MyClass實例可以具有相同的哈希碼。
  7. ISearchKey的哈希碼值僅基於其ICoreKey和IPartialKey值。 即ISearchKey哈希碼可能與匹配的MyClass實例的哈希碼不同。 (旁注:在我第一次遇到問題的情況下,ISearchKey的IPartialKey值與MyClass完整鍵匹配,因此GetHashCode方法將為ISearchKey和MyClass返回相同的值。我包括了額外的復雜度,以更好地說明基礎邏輯關於我在做什么。)
  8. _coreKeyComparer.GetHashCode方法僅使用ICoreKey值返回匹配的ISearchKey和MyClass實例的相同值。
  9. _coreKeyComparer.Equals方法分別將參數轉換為MyClass和ISearchKey,如果它們的IPartialKey值匹配,則返回true。 (附帶說明:_coreKeyComparer已經過嚴格測試,可以正常工作。)

我希望兩個集合之間的連接會產生類似以下結果:

{searchKey_a, myClass_a1},
{searchKey_a, myClass_a2},
{searchKey_a, myClass_a3},
{searchKey_b, myClass_b1},
{searchKey_b, myClass_b2},
{searchKey_c, myClass_c1},
{searchKey_c, myClass_c2},
{searchKey_c, myClass_c3},
{searchKey_c, myClass_c4},
etc....

即同一個ISearchKey實例將發生多次,對於它加入的每個匹配的MyClass實例一次。

但是當我執行從searchKeys到_mySet的聯接時:

        var matchedPairs = searchKeys
          .Join(
            _mySet,
            searchKey => searchKey,
            myClass => myClass,
            (searchKey, myClass) => new {searchKey, myClass},
            _coreKeyComparer)
            .ToList();

每個searchKeyClass實例只有一個MyClass實例。 即matchedPairs集合看起來像:

    {searchKey_a, myClass_a1},
    {searchKey_b, myClass_b1},
    {searchKey_c, myClass_c1},
etc....

但是,如果我取消連接,請從_mySet轉到searchKeys:

   var matchedPairs = _mySet
          .Join(
            searchKeys,
            myClass => myClass,
            searchKey => searchKey,
            (myClass, searchKey) => new {searchKey, myClass},
            _coreKeyComparer)
            .ToList();

我得到正確的matchPairs集合。 _mySet中的所有匹配記錄都將與它們匹配的searchKey一起返回。

我檢查了文檔並檢查了多個示例,沒有看到為什么searchKeys-to_mySet連接給出錯誤答案的原因,而_mySet-to-searchKeys提供正確/不同答案的原因。

(旁注:我還嘗試了將groupJoin從searchKeys到_myset並進行相似的結果。即,每個searchKeyClass實例最多可以從_mySet找到一個結果。)

我要么不明白Join方法應該如何工作,要么Join與HashSet的工作方式與List或其他類型的集合不同。

如果是前者,我需要澄清一下,這樣以后在使用Join時就不會犯錯誤。

如果是后者,則此不同行為是.NET錯誤,還是HashSet的正確行為?

假定行為正確,我將不勝感激有人解釋了此(意外)Join / HashSet行為背后的基本邏輯。

為了清楚起見,我已經修復了我的代碼,以便它返回正確的結果,我只想了解為什么最初得到錯誤的結果。

您的錯誤幾乎可以肯定地存在於問題中沒有顯示的大量代碼中。 我的建議是將您的程序簡化為可能產生該錯誤最簡單程序 這樣一來,您就可以找到錯誤,或者生成一個非常簡單的程序,可以將所有問題發布到問題中,然后我們可以對其進行分析。

假定行為正確,我將不勝感激有人解釋了此(意外)Join / HashSet行為背后的基本邏輯。

由於我不知道意外行為是什么,所以我無法說出它為什么發生。 但是,我可以准確地說出Join作用,也許會有所幫助。

Join需要以下內容:

  • “外部”集合-的接收器Join
  • “內部”集合-擴展方法的第一個參數
  • 兩個密鑰提取器,用於從外部和內部集合中提取密鑰
  • 一個投影,采用鍵匹配的內部和外部集合的成員,並產生該匹配的結果
  • 比較兩個鍵是否相等的比較操作。

這是Join工作方式。 (這在邏輯上是會發生的;實際的實現細節已得到一些優化。)

首先,我們僅對“內部”集合進行一次迭代。

對於內部集合的每個元素,我們提取其鍵,然后形成一個多字典,該字典將鍵映射到內部集合中所有鍵的集合,鍵選擇器在該集合中生成該鍵。 使用提供的比較對鍵進行相等性比較。

因此,我們現在從TKeyIEnumerable<TInner>進行查找。

其次,我們僅對一次“外部”集合進行迭代。

對於外部集合的每個元素,我們提取其鍵,並再次使用提供的鍵比較在多字典中對該鍵進行查找。

然后,我們在內部集合的每個匹配元素上進行嵌套循環,在外部/內部對上調用投影,然后得出結果。

也就是說, Join行為類似於以下偽代碼實現:

static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>
  (IEnumerable<TOuter> outer, 
  IEnumerable<TInner> inner, 
  Func<TOuter, TKey> outerKeySelector, 
  Func<TInner, TKey> innerKeySelector, 
  Func<TOuter, TInner, TResult> resultSelector, 
  IEqualityComparer<TKey> comparer) 
{
  var lookup = new SomeMultiDictionary<TKey, TInner>(comparer);
  foreach(TInner innerItem in inner)
  {
    TKey innerKey = innerKeySelector(innerItem);
    lookup.Add(innerItem, innerKey);
  }
  foreach (TOuter outerItem in outer) 
  {
    TKey outerKey = outerKeySelector(outerItem);
    foreach(TInner innerItem in lookup[outerKey])
    {
      TResult result = resultSelector(outerItem, innerItem);
      yield return result;
    }
  }
}

一些建議:

  • 替換所有的GetHashCode實現,使它們返回0 ,然后運行所有測試。 他們應該通過! GetHashCode返回零始終是合法的。 這樣做幾乎肯定會破壞您的表現 ,但絕對不能破壞您的正確性 如果您需要 GetHashCode的特定非零值,那么您將遇到一個錯誤。
  • 測試您的密鑰比較,以確保它是有效的比較。 它必須遵守三個相等的規則:(1)自反性:事物始終等於自身;(2)對稱性: AB的相等性必須與BA相同;(3)可傳遞性:如果A等於BB等於C那么A必須等於C 如果不滿足這些規則,那么Join可能會表現異常。
  • SelectManyWhere代替Join 那是:

    from o in outer join i in inner on getOuterKey(o) equals getInnerKey(i) select getResult(o, i)

可以改寫成

from o in outer
from i in inner
where keyEquality(getOuterKey(o), getInnerKey(i))
select getResult(o, i)

該查詢比連接版本 ,但從邏輯上講完全相同。 再次,運行測試。 你得到相同的結果嗎? 如果不是,則說明您的邏輯中存在錯誤

同樣,我不能足夠強調您的態度,即“給定哈希表時聯接可能被破壞”的態度使您無法找到錯誤。 加入沒有中斷。 該代碼十年來沒有發生變化,它非常簡單,並且在我們第一次編寫時是正確的。 您的復雜而神秘的鍵比較邏輯更有可能在某處被破壞了。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM