泛型 List<T>.Contains() 中的值相等性与引用相等性

6

试图简化这个问题的第三次尝试:

通用的List<T>可以包含任何类型 - 值类型或引用类型。当检查列表是否包含对象时,.Contains()使用类型T的默认EqualityComparer<T>并调用.Equals()(我的理解是这样的)。如果没有定义EqualityComparer,则默认比较器将调用.Equals()。默认情况下,.Equals()调用.ReferenceEquals(),因此只有在列表中包含完全相同的对象时,.Contains()才会返回true。

直到你需要覆盖.Equals()以实现值相等性,此时默认比较器会认为两个对象具有相同的值即相同。我想不出一个引用类型需要这种行为的例子。

@Enigmativity所说的是,实现IEqualityComparer<StagingDataRow>将为我的类型化DataRow提供一个默认的相等比较器,该比较器将代替Object的默认比较器 - 允许我在StagingDataRow.Equals()中实现值相等逻辑。

问题:

  1. 我理解得对吗?
  2. 我能保证.NET框架中的所有内容都会调用EqualityComparer<StagingDataRow>.Equals()而不是StagingDataRow.Equals()吗?
  3. IEqualityComparer<StagingDataRow>.GetHashCode(StagingDataRow obj)应该针对什么进行哈希,是否应该返回与StagingDataRow.GetHashCode()相同的值?
  4. 传递给IEqualityComparer<StagingDataRow>.GetHashCode(StagingDataRow obj)的是什么?我要查找的对象还是列表中的对象?两者都有?一个实例方法接受自身作为参数会很奇怪...

一般来说,在覆盖.Equals()时如何区分值相等性和引用相等性?



引发这个问题的原始代码行:

//  For each ID, a collection of matching rows
Dictionary<string, List<StagingDataRow>> stagingTableDictionary;


StagingTableMatches.AddRange(stagingTableDictionary[perNr].Where(row => !StagingTableMatches.Contains(row)));

.


1
你的问题真的很难理解。你是打算从 DataRow 继承并覆盖相等性方法吗?还是你计划创建一个自定义的 IEqualityComparer?你在哪里更改了 DataRow 中的数据?需要根据值比较 DataRows 的代码在哪里? - Yacoub Massad
2
听起来你只需要创建自己的 IEqualityComparer<DataRow> 实例并在查询中使用它。 - Enigmativity
@YacoubMassad,我已尽力提前总结了问题。如果还不清楚,请问吧。谢谢您的关注。 - James King
你的问题还不是很清楚。我认为@Enigmativity可能是正确的 - 你不需要(也不想)在这里重写Equals。正如Yacoub所问,比较它们的代码在哪里?你提供的代码似乎都与你的问题无关。提供一个最小可复现示例会很有用。 - Charles Mager
3
дёәд»Җд№ҲдёҚдҪҝз”ЁеёҰжңүзӣёзӯүжҜ”иҫғеҷЁзҡ„Enumerable.Contains(ж–№жі•пјҲhttps://msdn.microsoft.com/en-us/library/bb339118(v=vs.100).aspxпјүпјҢиҝҷж ·е°ұж— йңҖе…іеҝғй»ҳи®ӨжҜ”иҫғеҷЁжҳҜд»Җд№ҲгҖӮ - Scott Chamberlain
显示剩余9条评论
2个回答

4
好的,首先让我们澄清一些误解:
默认情况下,.Equals() 调用 .ReferenceEquals(),因此只有列表包含完全相同的对象时,.Contains() 才会返回 true。
这是正确的,但仅适用于引用类型。值类型将默认实现一个非常缓慢的基于反射的 Equals 函数,因此最好覆盖它。
我想不出任何一种情况,其中对于引用类型这样做是可取的。
哦,我相信你可以...例如,String 就是一个引用类型 :)
从 @Enigmativity 那里得到的信息是,实现 IEqualityComparer<StagingDataRow> 将为我的类型化 DataRow 提供一个默认的相等比较器,该比较器将替代 Object 的默认比较器 - 这使我能够在 StagingDataRow.Equals() 中实现值相等逻辑。
嗯...不是这样的。 IEqualityComaprer<T> 是一个接口,它允许您将相等比较委托给另一个对象。如果您希望类具有不同的默认行为,则实现 IEquatable<T>,并为了一致性也将 object.Equals 委托给它。实际上,重写 object.Equalsobject.GetHashCode 就足以更改默认的相等比较行为,但是实现 IEquatable<T> 还有其他好处:
  • 它使您的类型具有自定义相等比较逻辑 - 可以考虑自我记录代码。
  • 它提高了值类型的性能,因为它避免了不必要的装箱(这发生在 object.Equals 中)。
所以,针对您的实际问题:
我是否正确理解了?
您似乎仍然对此有些困惑,但不用担心 :)
Enigmativity 实际上建议您创建一个实现 IEqualityComparer<T> 的不同类型。看起来您误解了那部分内容。
我是否保证 .NET 框架中的所有内容都会调用 EqualityComparer<StagingDataRow>.Equals() 而不是 StagingDataRow.Equals()
默认情况下,(正确编写的)框架数据结构将委托相等比较给 EqualityComparer<StagingDataRow>.Default,后者将进一步委托给 StagingDataRow.Equals

IEqualityComparer<StagingDataRow>.GetHashCode(StagingDataRow obj)应该散列哪些内容?它返回的值是否应与StagingDataRow.GetHashCode()相同?

不一定。它应该是自洽的:如果myEqualitycomaprer.Equals(a, b),则必须确保myEqualitycomaprer.GetHashCode(a) == myEqualitycomaprer.GetHashCode(b)

它可能与StagingDataRow.GetHashCode使用相同的实现,但并非一定。

IEqualityComparer<StagingDataRow>.GetHashCode(StagingDataRow obj)传递的是什么?我正在寻找的对象还是列表中的对象?两者都有吗?将实例方法接受自身作为参数似乎很奇怪...

好吧,现在我希望您已经理解实现IEqualityComparer<T>的对象是一个不同的对象,因此这应该是有意义的。


请阅读我在Using of IEqualityComparer interface and EqualityComparer class in C#中的答案,了解更深入的信息。


谢谢进一步的澄清……我现在明白Enigmativity的意思了,即 IEqualityComparer 属于一个单独的类(因此不是我正在寻找的工具)。我不担心值类型……实际上,这个问题特定于引用类型(字符串不算,我不能从它继承 :))。但即使我在我的类中实现/覆盖 IEquatable,我只能选择一个“equals”——值或引用。假设它们可以互换似乎对我来说是一个有缺陷的设计。 - James King
你必须决定为你的引用类型给予哪种语义。我同意大多数情况下你应该使用引用相等语义(最少惊讶原则),但是对于其他情况,值类型语义可能更有意义(例如string)。请记住,你可以通过显式使用ReferenceEquals来强制使用引用语义,并且如果你需要处理几种不同的相等比较类型(如区分大小写/不区分大小写的字符串比较),你将不得不使用IEqualityComparer<T>,以便告诉它你想要哪一个。 - Lucas Trzesniewski
是的,我的结论是我必须保留默认的.Equals(),因为框架默认使用引用相等。因此,我的值相等测试必须在一个单独的方法中进行。虽然这样可以工作,但有点麻烦。谢谢。 - James King
该框架对任何自定义类型都不做任何假设。您不必保留引用相等性,也没有这样的指导方针。如果您的其余代码假定如此,那当然是另一回事,但是该框架只是……不关心。 - Lucas Trzesniewski

2
我理解得对吗?
部分正确 - “默认” IEqualityComparer 将按顺序使用以下内容:
1. IEquatable 的实现 2. 重写的 Equals(object) 3. 对于引用类型,基础的 object.Equals(object) 是引用相等性。
我认为你混淆了自定义类型中定义“相等性”的两种不同方法。一种是通过实现 IEquatable 来定义,这允许类型的一个实例确定它是否与另一个相同类型的实例“相等”。
另一种是 IEqualityComparer,它是一个独立的接口,用于确定该类型的两个实例是否相等。
因此,如果您的 Equals 定义应该适用于“每当比较两个实例时”,那么请实现 IEquatable,以及重写 Equals(在实现 IEquatable 后通常很简单)和 GetHashCode。
如果您的“相等”定义仅适用于特定用例,请创建一个不同的类来实现 IEqualityComparer,然后将其实例传递给您希望应用于该定义的任何类或方法。
我能保证 .NET Framework 中的所有内容都会调用 EqualityComparer.Equals() 而不是 StagingDataRow.Equals() 吗?
不会 - 只有接受 IEqualityComparer 实例作为参数的类型和方法才会使用它。
IEqualityComparer.GetHashCode(StagingDataRow obj) 应该针对什么进行哈希,它应该返回与 StagingDataRow.GetHashCode() 相同的值吗?
它将计算传递进来的对象的哈希码。它不会将哈希码与任何内容“比较”。它不一定要返回与重写的 GetHashCode 相同的值,但它必须遵循 GetHashCode 的规则,特别是两个“相等”的对象必须返回相同的哈希码。
一个实例方法接受自身作为参数似乎有些奇怪...
因此,IEqualityComparer 通常在 不同的 类上实现。请注意,IEquatable<T> 没有 GetHashCode() 方法,因为 它不需要。它假设 GetHashCode 被重写以匹配 object.Equals 的重写,这应该与强类型实现的 IEquatable<T> 相匹配。 底线 如果您希望您定义的“相等”是该类型的默认值,请实现 IEquatable<T> 并覆盖 EqualsGetHashCode。如果您想要一个特定用例的“相等”定义,则创建一个实现 IEqualityComparer<T>不同类,并将其实例传递给需要使用该定义的任何类型或方法。
另外,我要指出你很少直接调用这些方法(除了 Equals)。它们通常由使用它们的方法(如 Contains)调用,以确定两个对象是否“相等”或获取项目的哈希码。

明白了,或者说至少理解了一半。我感觉自己像是在徒劳地挣扎,试图解释这个问题,而实际上问题就在于覆盖.Equals()以给我值相等性会破坏所有假定.Equals()意味着相同对象的东西。一个类的两个实例可以具有值相等性并且在同一个List<T>中,但是.Contains()不应将它们视为相等。我实现了IEquatable,但我不希望将IEquatable.Equals()用于应该匹配实例本身而不是它们的值的查找。 - James King
那么,每个使用我的类的人都需要知道,在需要进行比较的任何地方都必须使用我的自定义比较器,如果他们忘记了,将会出现难以追踪的微妙和间歇性错误。我有意在设计中构建非预期的副作用。 - James King
我理解你的意思,大概12小时前我已经准备好放弃并实现一个不同名称的“Equals”函数来进行值相等性比较,但我真的希望有一个更清晰的解决方案...我想不出任何一种情况,我会想要重写.Equals()并希望使用值相等性从集合中检索不同的实例。我一定漏掉了什么重要的东西 - 这不可能是一个罕见的场景。 - James King
@JamesKing,这就是我感到困惑的地方 - 在你最初的问题中,你说你想通过它们的值来比较对象,所以显然你确实有一个有效的用例。最终的结论是你可以更改默认设置或在单独的类中实现它。选择你的方式... - D Stanley
是的,这就是我得到的 - 要么一个要么另一个 :( 没有代码转储很难清楚地表达... 当我将对象添加到集合中时,我通过匹配值进行比较(用户记录在多个数据源中)。 但是我有三个唯一标识符,没有一个是必需的,并且所有标识符都可以在用户就业期间更改...所以我必须说“添加到集合中,其中行不存在”(因为相同的行可以在多个ID上匹配)。 我基本上被束缚住了,一只脚被钉在头后面。 : | 如果您想要,可以重新发布更大的代码片段。 - James King
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接