提问人:Neo 提问时间:3/2/2018 最后编辑:Neo 更新时间:3/16/2018 访问量:2897
是否应该在可变类型上为 IEquatable<T> 实现 GetHashCode?
Should GetHashCode be implemented for IEquatable<T> on mutable types?
问:
我正在实现,但我很难就可变类的覆盖达成共识。IEquatable<T>
GetHashCode
以下资源都提供了一个实现,如果对象发生更改,则在该实现中将返回不同的值:GetHashCode
- https://stackoverflow.com/a/13906125/197591
- https://csharp.2000things.com/tag/iequatable/
- http://broadcast.oreilly.com/2010/09/understanding-c-equality-iequa.html
但是,此链接指出不应为可变类型实现,因为如果对象是集合的一部分,则可能会导致不良行为(这也是我的理解)。GetHashCode
有趣的是,MSDN 示例实现了仅使用不可变属性,这符合我的理解。但我对为什么其他资源没有涵盖这一点感到困惑。他们只是错了吗?GetHashCode
如果一个类型根本没有不可变的属性,编译器会警告我重写时缺少该属性。在这种情况下,我应该实现它并调用或禁用编译器警告,还是我错过了某些内容,应该始终被覆盖和实现?事实上,如果建议不应该为可变类型实现,为什么还要为不可变类型实现呢?与默认实现相比,它只是为了减少冲突,还是实际上增加了更多有形的功能?GetHashCode
Equals(object)
base.GetHashCode()
GetHashCode
GetHashCode
GetHashCode
总结一下我的问题,我的困境是,在可变对象上使用意味着如果对象上的属性发生变化,它可以在对象的生命周期内返回不同的值。但是,不使用它意味着将失去比较可能等效的对象的好处,因为它将始终返回唯一值,因此集合将始终回退到用于其操作。GetHashCode
Equals
输入此问题后,“类似问题”框中弹出了另一个问题,似乎涉及同一主题。答案似乎非常明确,因为在实现中只应使用不可变属性。如果没有,那就干脆不要写。 尽管不在 O(1) 性能下,但仍能正常工作。GetHashCode
Dictionary<TKey, TValue>
答:
这完全取决于你说的是哪种类型。对于我的回答,我将假设您正在谈论基于,特别是我将针对 .NET 和计算解决它。collection
Hash Table
collections
Dictionary
Key
因此,确定修改时会发生什么(假设您是一个执行自定义计算的类)的最佳方法是查看 .NET 源代码。从.NET源代码中,我们可以看到你的现在被包装成结构体,该结构体携带的值是计算的。这意味着,如果在添加键的时间之后更改值,它将无法再在 中找到值。key
key
HashCode
key value pair
Entry
hashcode
addition
HashCode
dictionary
证明这一点的代码:
static void Main()
{
var myKey = new MyKey { MyBusinessKey = "Ohai" };
var dic = new Dictionary<MyKey, int>();
dic.Add(myKey, 1);
Console.WriteLine(dic[myKey]);
myKey.MyBusinessKey = "Changing value";
Console.WriteLine(dic[myKey]); // Key Not Found Exception.
}
public class MyKey
{
public string MyBusinessKey { get; set; }
public override int GetHashCode()
{
return MyBusinessKey.GetHashCode();
}
}
所以要回答你的问题。您希望具有计算所依据的不可变值。hashcode
还有一点,对于自定义类,如果不覆盖将基于 .因此,对于基础值相同的不同对象返回相同的问题,可以通过方法和根据业务键计算来减轻。例如,您将有两个字符串属性,用于计算哈希码和调用基方法。这将保证 的相同基础值相同。hashcode
GetHashCode
object
hashcode
overriding GetHashCode
HashCode
concat
strings
string
GetHashCode
hashcode
object
评论
GetHashCode
base.GetHashCode
GetHashCode
可变类在字典和其他依赖于 GetHashCode 和 Equals 的类上工作得非常糟糕。
在您描述的场景中,对于可变对象,我建议以下方法之一:
class ConstantHasCode: IEquatable<ConstantHasCode>
{
public int SomeVariable;
public virtual Equals(ConstantHasCode other)
{
return other.SomeVariable == SomeVariable;
}
public override int GetHashCode()
{
return 0;
}
}
或
class ThrowHasCode: IEquatable<ThrowHasCode>
{
public int SomeVariable;
public virtual Equals(ThrowHasCode other)
{
return other.SomeVariable == SomeVariable;
}
public override int GetHashCode()
{
throw new ApplicationException("this class does not support GetHashCode and should not be used as a key for a dictionary");
}
}
对于第一种情况,Dictionary (几乎)按预期工作,在查找和插入时会受到性能影响:在这两种情况下,字典中已有的每个元素都会调用 Equals,直到比较返回 true。您实际上是在恢复列表的性能
第二种是告诉程序员将使用你的类“不,你不能在字典中使用它”的方法。 不幸的是,据我所知,没有方法可以在编译时检测到它,但是当代码第一次向字典添加元素时,这将失败,很可能在开发时很早就失败了,而不是那种只发生在生产环境中的错误,具有不可预测的输入集。
最后但并非最不重要的一点是,忽略“可变”问题并使用成员变量实现 GetHashCode:现在您必须意识到,当它与 Dictionary 一起使用时,您不能自由修改类。在某些情况下,这是可以接受的,而在其他情况下则不然
评论
GetHashCode
经过大量讨论并阅读了有关该主题的其他 SO 答案,最终是这个 ReSharper 帮助页面为我总结得很好:
GetHashCode() 方法的 MSDN 文档并不明确要求重写此方法返回在对象生存期内永不更改的值。具体来说,它说:
只要不修改确定对象的 Equals 方法的返回值的对象状态,对象的 GetHashCode 方法就必须一致地返回相同的哈希代码。
另一方面,它说哈希代码不应该改变,至少当你的对象在集合中时:
*您可以覆盖不可变引用类型的 GetHashCode。通常,对于可变引用类型,仅当满足以下条件时才应重写 GetHashCode:
- 您可以从不可变的字段计算哈希代码;或
- 您可以确保可变对象的哈希代码在对象包含在依赖于其哈希代码的集合中时不会更改。
但是为什么首先需要覆盖 GetHashCode() 呢?通常,如果你的对象要在 Hashtable 中使用,作为字典中的键等,你会这样做,并且很难预测你的对象何时会被添加到集合中以及它会在那里保留多长时间。
综上所述,如果您想安全起见,请确保重写 GetHashCode() 在对象的生命周期内返回相同的值。ReSharper 将通过指向 GetHashCode() 实现中的每个非只读字段或非 get only 属性来帮助您。如果可能的话,ReSharper 还会建议快速修复,使这些成员成为只读/只获取。
当然,它并不建议如果无法快速修复该怎么做。但是,它确实表明,这些快速修复应该只在“可能的情况下”使用,这意味着可以抑制检查。Gian Paolo 对此的回答建议抛出一个异常,该异常将阻止该类被用作密钥,如果它无意中被用作密钥,则会在开发早期出现。
但是,在其他情况下使用,例如,当对象的实例作为参数传递给模拟方法设置时。因此,唯一可行的选择是使用可变值实现,并将责任放在代码的其余部分,以确保对象在用作键时不会发生突变,或者根本不将其用作键。GetHashCode
GetHashCode
评论
GetHashCode
Dictionary<TKey, TValue>
GetHashCode()
GetHashCode
GetHashCode