需要帮助,以结合使用LINQ Join和HashSet <t>

时间:2019-01-03 15:47:58

标签: c# linq join inner-join hashset

我使用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方法返回     使用以下方法匹配ISearchKey和MyClass实例的值相同     仅其ICoreKey值。
  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提供正确/不同的答案。

(附带说明:我还尝试了从searchKeys到_myset的GroupJoin并获得相似的结果。即,每个searchKeyClass实例最多从_mySet找到一个结果。)

要么我不明白Join方法应该如何工作,要么Join在HashSet中的工作方式与List或其他类型的集合不同。

如果是前者,我需要澄清一下,这样以后就不会再使用Join出错了。

如果是后者,则此不同行为是.NET错误,还是HashSet的正确行为?

假设行为正确,我将不胜感激有人解释了此(意外)Join / HashSet行为背后的基本逻辑。

请明确说明,我已经修复了代码,可以返回正确的结果,我只是想了解为什么最初得到的结果不正确。

1 个答案:

答案 0 :(得分:5)

几乎可以肯定,您的错误在您未在问题中显示的大量代码中。我的建议是将您的程序简化为可能会产生错误的最简单程序。这样一来,您就可以找到错误,或者生成一个非常简单的程序,可以将所有问题发布到问题中,然后我们可以对其进行分析。

  

假设行为正确,那么不胜感激的人会解释这种(意外的)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的相等性必须与B相同,并且A,(3)可传递性:如果A等于B,而B等于C,则A必须等于C。如果不满足这些规则,那么Join的行为可能会很奇怪。
  • JoinSelectMany替换Where。那就是:

    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)

该查询比连接版本慢 ,但在逻辑上 完全相同。再次,运行测试。你得到相同的结果吗?如果不是,您的逻辑中存在一个错误

同样,我不能十分强调您的态度,即“给定哈希表时联接可能被破坏”的态度使您无法找到错误。加入没有中断。该代码十年来没有发生变化,它非常简单,并且在我们第一次编写时是正确的。更有可能的是,您复杂而神秘的键比较逻辑在某处被破坏了。