为什么 GetHashCode 应该实现与 Equals 相同的逻辑?

Why should GetHashCode implement the same logic as Equals?

this MSDN 页面上写着:

Warning:

If you override the GetHashCode method, you should also override Equals, and vice versa. If your overridden Equals method returns true when two objects are tested for equality, your overridden GetHashCode method must return the same value for the two objects.

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

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

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 方法将为每个 return 不同的值。这两个温度对象是相等的,但它们也不相同,所以它们具有不同的哈希码是不是不正确?

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

如果您实施了 GetHashCode() 实施 Equals(),则框架将求助于使用 引用相等性 检查是否相等,这将导致每个 实例 产生碰撞。

如果您实现了 Equals() 没有 实现 GetHashCode(),您可能 运行 遇到这样的情况,即您有两个对象相等,但会产生不同的哈希码,这意味着它们会在您的哈希 table 中维护自己独立的值。这可能会使使用您的 class.

的任何人感到困惑

至于哪些对象被视为相等,这取决于您。如果我根据温度创建散列 table,我是否可以使用其摄氏度或华氏度值来引用同一项目?如果是这样,他们需要产生相同的散列值 and Equals() 需要 return true.

更新:

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

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

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

两个相等的对象应该 return 相同的哈希码(两个不同的对象也可以 return 相同的哈希码,但这是一个冲突)。

在您的情况下,您的 equals 和哈希码实现都不是很好。问题是对象的 "real value" 取决于参数:没有单个 属性 定义对象的值。您只存储初始单位以进行相等比较。

那么,您为什么不对 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:这没有意义,因为为了取回值,你总是会传递一个值。只有在初始转换时传递它才有意义。

Warning:

If you override the GetHashCode method, you should also override Equals, and vice versa. If your overridden Equals method returns true when two objects are tested for equality, your overridden GetHashCode method must return the same value for the two objects.

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

If two object return true from Equals they will return the same hash code

并且:

If two objects return different hash codes they are NOT equal

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

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

在您特定的 class 中,您需要决定,当单位不同时 Equals returns false,或者 GetHashCode returns 相同的哈希码,无论单位如何。不能两全其美。

所以你要么这样做:

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));
}

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