我正在尝试实现IEqualityComparer<T>
,以便将Except
用于复杂类型的集合。我在调试时遇到了一个奇怪的(或我不知道的常见问题)问题。我有两个不同数量的收藏品,如下所示。
{A, B, C, D, E}.Except({A, B}, new CustomComparer()).ToList()
通过添加ToList()
,我可以调试覆盖的Equals(x, y)
。直到项目C
,它才像预期的那样工作。在项目B
之后,Except将C
和D
发送到Equals(x, y)
,所以我无法区分这些元素属于第一个集合还是其中一个属于第二个集合。
下面是我的IEqualityComparer实现。DifferenceHighlighter是一种回调方法,它为我提供了在调用方位置收集差异的方法。
public SubmoduleListComparer(
Action<FormerGsdmlComparison.SubModuleListDifferenceContainer, string, string> callBack,
string firstFileName,
string secondFilename)
{
DifferenceHighlighter = callBack;
m_FirstFileName = firstFileName;
m_SecondFileName = secondFilename;
}
public bool Equals(Submodule x, Submodule y)
{
bool areEqual = true;
if (x == null || y == null) return false;
var submoduleDifferences = new FormerGsdmlComparison.SubModuleListDifferenceContainer
{
file1 = new FormerGsdmlComparison.Submodule
{
orderNumber = x.OrderNumber,
submoduleId = x.Id,
submoduleIdentNumber = x.SubmoduleIdentNumber
}
};
if (x.Id != y.Id)
{
submoduleDifferences.file2.submoduleId = y.Id;
areEqual = false;
}
if (x.OrderNumber != y.OrderNumber)
{
submoduleDifferences.file2.orderNumber = y.OrderNumber;
areEqual = false;
}
if (x.SubmoduleIdentNumber != y.SubmoduleIdentNumber)
{
submoduleDifferences.file2.submoduleIdentNumber = y.SubmoduleIdentNumber;
areEqual = false;
}
if (!areEqual)
{
DifferenceHighlighter(submoduleDifferences, m_FirstFileName, m_SecondFileName);
}
return areEqual;
}
正如我上面提到的;当第二个集合的项的迭代结束时,我期望Except()
发送null
。相反,它将第一个集合中的两个元素发送到Equals(x, y)
。这是LINQ-Except的默认行为,我应该做更多的检查,还是我遗漏了什么?
编辑
first
集合包含51个元素,second
集合仅包含7个元素。在从两个集合向Equals(x,y)发送7个项之后;从first
集合开始发送顺序项目除外。例如:
这是两个集合上第一个项目的调试视图
上图正是我所期待的。前两项属于Equals方法。但在第7次迭代之后;Equals(x, y)
上的项目就是这样。
second
集合没有这些项。以上项目是first
系列的第8和第9个元素。所以我的DifferenceHighlighter假设这是两个集合之间的差异。
这是预期的行为;Except
使用仅包含uinque项目的集合(而非袋子)进行操作;因此Except
只返回不同的项目:
var demo = new int[] {1, 1}
.Except(new int[0])
.ToList();
Console.Write(string.Join(" ", demo));
结果:
1
在您的案例中,Except
测试项目C
和D
(第一个集合中的都)正是为了确保只返回不同的项目:
https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,e289e6c98881b2b8
static IEnumerable<TSource> ExceptIterator<TSource>(
IEnumerable<TSource> first,
IEnumerable<TSource> second,
IEqualityComparer<TSource> comparer) {
Set<TSource> set = new Set<TSource>(comparer);
foreach (TSource element in second)
set.Add(element);
foreach (TSource element in first)
// Here Except tries adding element from first
// and have to compare if the element has been in set already.
// in your case 'D' will be tested on A, B (which are in second)
// and 'C' which has been added earlier
if (set.Add(element))
yield return element;
}
如果您想要"first
中的所有项目(包括重复的),除了出现在second
中的项目",您可以手动创建HashSet<T>
,并放置一个简单的Where
:
var second = new MyType[] {A, B}; // or whatever IEnumerable<MyType>
...
// Items to exclude
HashSet<MyType> exclude = new HashSet<MyType>(second, new CustomComparer());
var result = first // {A, B, C, D, E}
.Where(item => !exclude.Contains(item)) // all items but appear in exclude - i.e. second
.ToList();
事实证明,我完全误解了Except()
的用途和用法。正如Chris的评论和Dmitry对Except()
解释的回答一样,最好使用Zip()
来迭代两个集合,检测差异并合并另一个集合(或其他无数选项)中的结果。Except
确实做到了。经过对Zip()
简单代码示例的快速研究,它也符合我的条件,如下所示:
foreach (var submoduleListPairs in firstFile.SubmoduleList.Zip(secondFile.SubmoduleList, (x, y) => new { x, y }))
{
if (submoduleListPairs.x != null && submoduleListPairs.y != null)
{
if (submoduleListPairs.x.SubmoduleIdentNumber != submoduleListPairs.y.SubmoduleIdentNumber)
{
//Add differences to result collection
}
//Do other comparisons like below
}
else if (submoduleListPairs.x == null)
{
//Notate that second collection contains an item which first one not on result collection
}
else if (submoduleListPairs.y == null)
{
//Notate that first collection contains an item which second one not on result collection
}
}
这可能不是Zip()
的最佳用法,但我想展示一下它是如何解决我的问题的,以及为什么我不应该使用Except()
进行比较。我想我有一个想法,IEqualityComparer和Except是LINQ比较问题的方法。
上次编辑
Zip()
的想法很有启发性,但内置的Zip()
会在其中一个集合因其用途(合并集合)而过时时停止。所以我又做了一次更深入的搜索,找到了这个很棒的So问题和答案。即使没有赞成票,这也是对上述答案的极大简化。