我使用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.
{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实例,都将发生一次。
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....
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行为背后的基本逻辑。
请明确说明,我已经修复了代码,可以返回正确的结果,我只是想了解为什么最初得到的结果不正确。
答案 0 :(得分:5)
几乎可以肯定,您的错误在您未在问题中显示的大量代码中。我的建议是将您的程序简化为可能会产生错误的最简单程序。这样一来,您就可以找到错误,或者生成一个非常简单的程序,可以将所有问题发布到问题中,然后我们可以对其进行分析。
假设行为正确,那么不胜感激的人会解释这种(意外的)Join / HashSet行为背后的潜在逻辑。
由于我不知道意外行为是什么,所以无法说出发生原因。但是,我可以准确地说出Join
的功能,也许会有所帮助。
Join
采用以下条件:
Join
的接收者。 Join
的工作方式如下。 (这在逻辑上是 ;实际的实现细节有所优化。)
首先,我们仅对一次“内部”集合进行迭代。
对于内部集合的每个元素,我们提取其键,然后形成一个多字典,该字典将键从键映射到内部集合中键选择器生成该键的所有元素的集合。使用提供的比较对键进行相等性比较。
因此,我们现在可以从TKey
到IEnumerable<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
非零值的情况,则说明存在错误。 A
和B
的相等性必须与B
相同,并且A
,(3)可传递性:如果A
等于B
,而B
等于C
,则A
必须等于C
。如果不满足这些规则,那么Join
的行为可能会很奇怪。用Join
和SelectMany
替换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)
该查询比连接版本慢 ,但在逻辑上 完全相同。再次,运行测试。你得到相同的结果吗?如果不是,您的逻辑中存在一个错误。
同样,我不能十分强调您的态度,即“给定哈希表时联接可能被破坏”的态度使您无法找到错误。加入没有中断。该代码十年来没有发生变化,它非常简单,并且在我们第一次编写时是正确的。更有可能的是,您复杂而神秘的键比较逻辑在某处被破坏了。