等于和哈希代码的最佳策略是什么?

53

我正在处理一个域模型,并思考在.NET中实现这两种方法的各种方式。你最喜欢的策略是什么?

这是我的当前实现:

public override bool Equals(object obj)
{
    var newObj = obj as MyClass;

    if (null != newObj)
    {
        return this.GetHashCode() == newObj.GetHashCode();
    }
    else
    {
        return base.Equals(obj);
    }
}

// Since this is an entity I can use its Id
// When I don't have an Id, I usually make a composite key of the properties
public override int GetHashCode()
{
    return String.Format("MyClass{0}", this.Id.ToString()).GetHashCode();
}

相关:https://dev59.com/rEzSa4cB1Zd3GeqPkS4l - Mark Seemann
10
你不能仅依靠 GetHashCode 的结果来确定 Equals - 当对象不同时,哈希码可能相同。你最好在 Equals 中比较你的 Ids。关于此问题的更多信息,请参见为什么在 C# 中重写 Equals 方法时重写 GetHashCode 很重要? - Blair Conrad
请记住,GetHashCode()主要用于性能重要的代码中(带有O(1)查找等列表)。您的实现已经相当缓慢,但是您可以在不改变太多的情况下加快速度:return ("MyClass" + this.Id).GetHashCode();(这只是您可能需要记住的内容)。 - Aidiakapi
@Aidiakapi 在任何情况下,基于连接字符串的哈希都可能是一个可怕的想法。 - ErikE
7个回答

39

领域驱动设计将实体(Entities)和值对象(Value Objects)进行了区分。这是一个很好的区分,因为它指导了如何实现Equals方法。

实体的相等性基于它们的ID相等。

值对象的相等性基于其所有(重要的)组成元素相等。

无论哪种情况,GetHashCode方法的实现都应基于用于确定相等性的相同值。换句话说,对于实体而言,哈希码应直接从ID中计算,而对于值对象,则应从所有组成值中计算。


您介意澄清在比较实体的ID时,拥有一个比较方法Equals()的好处是什么吗?有哪些使用情况?我在这里的SO上问过类似的问题,但到目前为止,我还没有得到一个明确说明为什么应该通过ID比较实体的答案。 - theDmi
1
@theDmi - 原因在于实体的定义特别指出它是唯一可识别的,可能由系统控制。至少对于一个包含数据和修改数据方法的实体,每个实体都将具有标识符,并且该标识符将是唯一的。如果我使用相同的数据和构造调用创建十个实体,则唯一的事物将是标识符。这就是为什么Evans说系统可以控制标识符的创建。个人而言,我总是让实体创建标识符以支持此功能。 - Joseph Ferris

14

这里没有一个回答真正满足我的需求。既然您已经说过不能使用Id进行相等性比较,且需要使用一组属性,那么这里有一种更好的方法来做到这一点。注意:我并不认为这是实现EqualsGetHashCode的最佳方式。这只是原始帖子中代码的更好版本。

public override bool Equals(object obj) {
   var myClass = obj as MyClass;

   if (myClass != null) {
      // Order these by the most different first.
      // That is, whatever value is most selective, and the fewest
      // instances have the same value, put that first.
      return this.Id == myClass.Id
         && this.Name == myClass.Name
         && this.Quantity == myClass.Quantity
         && this.Color == myClass.Color;
   } else {
      // This may not make sense unless GetHashCode refers to `base` as well!
      return base.Equals(obj);
   }
}

public override int GetHashCode() {
   int hash = 19;
   unchecked { // allow "wrap around" in the int
      hash = hash * 31 + this.Id; // assuming integer
      hash = hash * 31 + this.Name.GetHashCode();
      hash = hash * 31 + this.Quantity; // again assuming integer
      hash = hash * 31 + this.Color.GetHashCode();
   }
   return hash;
}

请见Jon Skeet的这个答案,以了解背后的一些原因。使用异或不好,因为各种数据集可能会导致相同的哈希值。使用质数的包装方法(如上面的种子值19和31,或者您选择的其他值)更能够将数据分割成具有较少冲突的"桶"。

如果您的任何值可以为空(null),请仔细考虑它们应该如何比较。您可以使用短路空值评估和空合并运算符。但是,请确保如果null应该被视为相等,则在它们为null时为不同的可空属性分配不同的哈希码。

此外,我不确定您的Equals实现是否有任何意义。当比较两个对象是否相等时,首先要比较它们的GetHashCode值。只有在这些值不同的情况下才会运行Equals方法(以便检测到哈希到相同值的两个不同对象)。由于您的GetHashCode实现不参考base,因此您的Equals方法可能没有参考的意义。具体来说,如果Equals可以为具有不同哈希码的两个对象返回true,则会出现严重的错误,等待着破坏事物。


这个答案几乎完美。除了Equals(object)应该调用一个重载的Equals(MyClass),它将在IEquatable<MyClass>中使用。此外,CLR现在有IStructuralEquatable<T>,我不知道它们之间的区别是什么。 - JAlex

6
假设实例相等是因为哈希码相等是错误的。我猜你的 GetHashCode 实现没问题,但我通常使用类似于这样的方法:
public override int GetHashCode() {
    return object1.GetHashCode ^ intValue1 ^ (intValue2 << 16);
}

3
我知道这是一个老问题,但你能解释一下你在这里使用的表达吗? - skjagini
1
@skjagini:请查看^运算符 - 异或,以及<<运算符 - 左移。有许多方法可以将多个数字值组合在一起以获得单个HashCode。异或有效地扭曲位; 但是它也有一些弱点。搜索GetHashCode实现,例如此SO帖子 - Nigel Touch
4
我真的搞不清楚你对@tucaz提出的问题所回答的要点是什么。 - Lord of the Goo

4
我偶然发现了这个旧问题,但是我认为没有任何答案清晰而简单地回答了@tucaz提出的原始问题。
我同意上面(或下面:D)分享的许多考虑,但是“问题点”被忽略了(我认为)。
假设:
- 实体需要相等 - 如果实体对象映射到相同的实体,则可以认为它们相等,即它们引用相同的“实体键” - @tucaz所展示的示例只提到了一个“Id”(请参见过度实现的GetHashCode())...更不用说有问题的Equals(...)了
我猜想一个直接的实现可能是:
public class MyEntity: IEquatable<MyEntity> {
    int Id;

    public MyEntity(int id){
        Id = id;
    }

    public override bool Equals(object obj) => Equals(obj as MyEntity);
    public bool Equals(MyEntity obj) => obj != null && Id == obj.Id;
    public override int GetHashCode() => Id;
}

那就是全部内容了!


3

哈希码可能会发生冲突,因此我认为它们不是比较相等性的好方法。相反,您应该比较使对象“相等”的基本值。如果您的相等性涵盖多个属性,请参见@Jon Skeet对此问题的答案:What is the best algorithm for an overridden System.Object.GetHashCode?,以获得更好的GetHashCode实现。如果只有一个属性,则可以重用其哈希码。


你说得没错,但在我看来,过于谨慎了。哈希码绝对不适合用于确定相等性。相同的对象必须具有相同的哈希码,但是不同的对象可能共享相同的哈希码。正如你所说,它们可能会发生碰撞。 - Bob Stine

1

0

我想基于以上回答和我的经验,看一些特定情况。

一个经验法则是,具有不同哈希码的两个实例应始终不相等,但如果它们具有相同的哈希码,则可能相等也可能不相等。GetHashCode() 用于快速区分实例,Equals() 用于验证相等性(无论对您来说意味着什么)。

此外,许多内置机制都会寻找 IEquatable<T> 的实现,因此最好声明一个重写了实际检查的 Equals(MyClass)

具有唯一 ID 的类

考虑一个具有唯一 ID 的类。然后 equals 操作将只检查 id。哈希也是如此,它完全依赖于id。

public class IdClass : IEquatable<IdClass>
{
    public int ID { get; } // Assume unique
    public string Name { get; }


    #region IEquatable Members
    /// <summary>
    /// Equality overrides from <see cref="System.Object"/>
    /// </summary>
    /// <param name="obj">The object to compare this with</param>
    /// <returns>False if object is a different type, otherwise it calls <code>Equals(IdClass)</code></returns>
    public override bool Equals(object obj)
    {
        if (obj is IdClass other)
        {
            return Equals(other);
        }
        return false;
    }

    /// <summary>
    /// Checks for equality among <see cref="IdClass"/> classes
    /// </summary>
    /// <param name="other">The other <see cref="IdClass"/> to compare it to</param>
    /// <returns>True if equal</returns>
    public virtual bool Equals(IdClass other)
    {
        if (other == null) return false;
        return ID.Equals(other.ID);
    }

    /// <summary>
    /// Calculates the hash code for the <see cref="IdClass"/>
    /// </summary>
    /// <returns>The int hash value</returns>
    public override int GetHashCode() => ID.GetHashCode();

    #endregion

}

带属性的类

这种情况与上面类似,但比较取决于两个或更多属性,并且需要在哈希码中不对称地组合。这将在下一个场景中更加明显,但思想是如果一个属性具有哈希A,另一个属性具有哈希B,则结果应该与第一个属性具有哈希B和另一个哈希A的情况不同。

public class RefClass : IEquatable<RefClass>
{
    public string Name { get; }
    public int Age { get; }


    #region IEquatable Members
    /// <summary>
    /// Equality overrides from <see cref="System.Object"/>
    /// </summary>
    /// <param name="obj">The object to compare this with</param>
    /// <returns>False if object is a different type, otherwise it calls <code>Equals(RefClass)</code></returns>
    public override bool Equals(object obj)
    {
        if (obj is RefClass other)
        {
            return Equals(other);
        }
        return false;
    }

    /// <summary>
    /// Checks for equality among <see cref="RefClass"/> classes
    /// </summary>
    /// <param name="other">The other <see cref="RefClass"/> to compare it to</param>
    /// <returns>True if equal</returns>
    public virtual bool Equals(RefClass other)
    {
        if (other == null) { return false; }
        return Name.Equals(other.Name)
            && Age.Equals(other.Age);
    }

    /// <summary>
    /// Calculates the hash code for the <see cref="RefClass"/>
    /// </summary>
    /// <returns>The int hash value</returns>
    public override int GetHashCode()
    {
        unchecked
        {
            int hc = -1817952719;
            hc = (-1521134295) * hc + Name.GetHashCode();
            hc = (-1521134295) * hc + Age.GetHashCode();
            return hc;
        }
    }

    #endregion

}

基于值的类(结构体)

这与上面的情况几乎相同,只是基于值的类型(struct声明)还需要重新定义==!=以调用equals方法。

public struct ValClass : IEquatable<ValClass>
{
    public int X { get; }
    public int Y { get; }

    #region IEquatable Members
    /// <summary>
    /// Equality overrides from <see cref="System.Object"/>
    /// </summary>
    /// <param name="obj">The object to compare this with</param>
    /// <returns>False if object is a different type, otherwise it calls <code>Equals(ValClass)</code></returns>
    public override bool Equals(object obj)
    {
        if (obj is ValClass other)
        {
            return Equals(other);
        }
        return false;
    }

    public static bool operator ==(ValClass target, ValClass other) { return target.Equals(other); }
    public static bool operator !=(ValClass target, ValClass other) { return !(target == other); }


    /// <summary>
    /// Checks for equality among <see cref="ValClass"/> classes
    /// </summary>
    /// <param name="other">The other <see cref="ValClass"/> to compare it to</param>
    /// <returns>True if equal</returns>
    public bool Equals(ValClass other)
    {
        return X == other.X && Y == other.Y;
    }

    /// <summary>
    /// Calculates the hash code for the <see cref="ValClass"/>
    /// </summary>
    /// <returns>The int hash value</returns>
    public override int GetHashCode()
    {
        unchecked
        {
            int hc = -1817952719;
            hc = (-1521134295) * hc + X.GetHashCode();
            hc = (-1521134295) * hc + Y.GetHashCode();
            return hc;
        }
    }

    #endregion

}

请注意,struct 应该是不可变的,并且在声明中添加 readonly 关键字是一个好主意。
public readonly struct ValClass : IEquatable<ValClass>
{
} 

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