【问题标题】:Why should GetHashCode implement the same logic as Equals?为什么 GetHashCode 应该实现与 Equals 相同的逻辑?
【发布时间】:2016-04-21 11:02:40
【问题描述】:

this MSDN 页面上写着:

警告:

如果你重写 GetHashCode 方法,你也应该重写 Equals,反之亦然。如果在测试两个对象是否相等时覆盖的 Equals 方法返回 true,则覆盖的 GetHashCode 方法必须为两个对象返回相同的值

我也看到了许多类似的建议,我可以理解,在覆盖 Equals 方法时,我也想覆盖 GetHashCode。不过据我所知,GetHashCode 与哈希表查找一起使用,这与相等检查不同。

这里有一个例子来帮助解释我想问的问题:

public class Temperature /* Immutable */
{
    public Temperature(double value, TemperatureUnit unit) { ... }

    private double Value { get; set; }
    private TemperatureUnit Unit { get; set; }

    private double GetValue(TemperatureUnit unit)
    {
        /* return value converted into the specified unit */
    }

    ...

    public override bool Equals(object obj)
    {
        Temperature other = obj as Temperature;
        if (other == null) { return false; }
        return (Value == other.GetValue(Unit));
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode() + Unit.GetHashCode();
    }
}

在这个例子中,两个温度对象被认为是相等的,即使它们内部没有存储相同的东西(例如 295.15 K == 22 摄氏度)。目前 GetHashCode 方法将为每个返回不同的值。这两个温度对象相等但也不相同,所以它们具有不同的哈希码是不正确的吗?

【问题讨论】:

  • 如果您永远不会将您的Temperature 对象存储在HashSetDictionary 中,那么您可以 忽略GetHashCode,但是你真的,真的不应该。这将是一种可怕的做法。
  • @IvanStoev:当然,这确实说明了为什么不应该这样做。您可能确信您没有将某些内容放入HashSet,但是,尤其是对于 LINQ 之类的东西,您不能确定其中一个的内部实现是否使用了 HashCode(几乎可以肯定是) .
  • 只是为了澄清我正在尝试实现两者。
  • @Ben:您确实需要区分在温度测量的物理世界中平等意味着什么,以及在 C# 代码世界中平等意味着什么。如果您的Equals 方法返回true,那么这两个对象必须 返回相同的哈希码。如果他们不这样做,您的代码将以许多令人沮丧的有趣方式中断......

标签: c# equality gethashcode


【解决方案1】:

当在哈希表中存储一个值时,例如Dictionary<>,框架将首先调用GetHashCode() 并检查哈希表中是否已经存在该哈希码的bucket。如果有,它将调用.Equals() 来查看新值是否确实等于 到现有值。如果不是(这意味着两个对象不同,但会产生相同的哈希码),那么您就有了所谓的冲突。在这种情况下,这个桶中的项目被存储为一个链表,并且检索到某个值变成了 O(n)。

如果你实现了GetHashCode(),但没有实现Equals(),框架将诉诸于使用引用相等来检查相等性,这将导致每个instance 创建碰撞。

如果您实现了Equals(),但没有实现了GetHashCode(),您可能会遇到这样一种情况,即您有两个相等的对象,但会产生不同的哈希码,这意味着它们会维护在您的哈希表中它们自己的单独值。这可能会使使用您的课程的任何人感到困惑。

至于哪些对象被认为相等,这取决于你。如果我根据温度创建一个哈希表,我是否可以使用摄氏或华氏值来引用同一个项目?如果是这样,它们需要产生相同的哈希值并且Equals()需要返回true。

更新:

让我们先回过头来看看哈希码的用途。在这种情况下,哈希码被用作一种快速识别两个对象是否最可能相等的方法。如果我们有两个具有不同哈希码的对象,我们就知道它们相等。如果我们有两个具有 same 哈希码的对象,我们知道它们很可能是相等的。我说最有可能是因为 int 只能用来表示几十亿个可能的值,而字符串当然可以包含查尔斯·狄更斯的全集,或者任意数量的可能值。 .NET 框架中的大部分内容都基于这些事实,使用您的代码的开发人员会假设事情的工作方式与框架的其余部分一致。

如果您有两个具有不同哈希码的实例,但有一个返回 true 的 Equals() 实现,那么您就违反了这个约定。然后,比较两个对象的开发人员可能会使用其中一个对象来引用哈希表中的键并 expect 以获取现有值。如果突然哈希码不同,则此代码可能会导致运行时异常。或者可能返回对完全不同对象的引用。

295.15k 和 22C 在您的程序域内是否相等是您的选择(在我看来,它们不是)。但是,无论您决定什么,相等的对象必须返回相同的代码。

【讨论】:

  • 如果你不覆盖 equals 它将默认引用相等。
  • 此外,当框架在字典中查找时,它会首先使用哈希码,因此您可以将295.15K存储在字典中,但是当您查找22C,找不到了。这是你想要的吗?也许,也许不是。
  • 是的,这正是我所期望的,295.15K 与 22C 不同,所以它应该存储在不同的存储桶中吧?
  • 如果是我,我不会像那样隐式地进行单位转换Temperature 类应该有一个 Unit 属性,并且以不同的单位测量它们的实例应该被视为相等。现在,您可以有一个 .ToCelsius().ToKelvin() 方法,并且 that 将返回一个相等的实例。
  • 同意。任何使用您的类 expects 对象的人都可以用来引用哈希表中的相同键。其他所有班级都以这种方式工作。如果你打破这条规则,你可能会在未来创造一些非常奇怪,真的难以追踪的错误。相同的哈希码表明对象可能相等,Equals 告诉您它们是否肯定相等。这两个概念需要齐头并进。
【解决方案2】:

两个相等的对象应该返回相同的 HashCode(两个不同的对象也可能返回相同的 hashcode,但这是冲突)。

在你的情况下,你的 equals 和你的 hashcode 实现都不是一个好的实现。问题在于对象的“真实值”取决于参数:没有定义对象值的单一属性。您只存储初始单元以进行相等比较。

那么,您为什么不确定您的TemperatureValue 是什么的内部定义?

我会像这样实现它:

public class Temperature
{
    public Temperature(double value, TemperatureUnit unit) { 
       Value = ConvertValue(value, unit, TemperatureUnit.Celsius);
    }

    private double Value { get; set; }

    private double ConvertValue(double value, TemperatureUnit originalUnit,  TemperatureUnit targetUnit)
    {
       /* return value from originalUnit converted to targetUnit */
    }
    private double GetValue(TemperatureUnit unit)
    {
       return ConvertValue(value, TemperatureUnit.Celsius, unit);
    }    
    public override bool Equals(object obj)
    {
        Temperature other = obj as Temperature;
        if (other == null) { return false; }
        return (Value == other.Value);
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
}

这样,您的内部Value 定义了两个对象是否相同,并且始终以相同的单位表示。

你并不关心Unit 对象有什么:这没有意义,因为要取回值,你总是会传递一个值。仅将其传递给初始转换才有意义。

【讨论】:

  • 不,他们不会平等。该值在构造函数中进行转换。存储的“值”将是摄氏度
  • 在 OP 示例中值是私有的,谁在乎你如何在内部存储它?
【解决方案3】:

警告

如果你重写 GetHashCode 方法,你也应该重写 Equals,反之亦然。如果在测试两个对象是否相等时重写的 Equals 方法返回 true,则重写的 GetHashCode 方法必须为这两个对象返回相同的值。

这是 .NET 库中的约定。它在编译时甚至在运行时都不会强制执行,但 .NET 库(可能还有任何其他外部库)中的代码希望此语句始终为真:

如果两个对象从Equals 返回true,它们将返回相同的哈希码

还有:

如果两个对象返回不同的哈希码,它们相等

如果您不遵循该约定,那么您的代码将会中断。更糟糕的是,它可能会以难以追踪的方式中断(例如将两个相同的对象放入字典中,或者从字典中获取与您预期不同的对象)。

所以,遵守约定,否则你会给自己带来很多痛苦。

在您的特定类中,您需要决定,要么Equals 在单位不同时返回 false,要么GetHashCode 无论单位如何都返回相同的哈希码。你不能同时拥有它。

所以你要么这样做:

public override bool Equals(object obj)
{
    Temperature other = obj as Temperature;
    if (other == null) { return false; }
    return (Value == other.Value && Unit == other.Unit);
}

或者你这样做:

public override int GetHashCode()
{
    // note that the value returned from ConvertToSomeBaseUnit
    // should probably be cached as a private member 
    // especially if your class is supposed to immutable
    return Value.ConvertToSomeBaseUnit().GetHashCode();
}

请注意,没有什么能阻止您也实施:

public bool TemperaturesAreEqual(Temperature other)
{
    if (other == null) { return false; }
    return (Value == other.GetValue(Unit));
}

当您想知道两个温度是否代表相同的物理温度时使用它,而不管单位如何。

【讨论】:

  • 感谢马特的回答,我想我一直都知道我必须遵守约定,但是当约定似乎难以理解时,我想了解更多关于为什么约定是这样的。
猜你喜欢
  • 2012-04-07
  • 1970-01-01
  • 1970-01-01
  • 2021-06-15
  • 2011-09-12
  • 1970-01-01
  • 2021-09-23
  • 2012-01-26
  • 1970-01-01
相关资源
最近更新 更多