Equals 和 GetHashCode

Equals 每个实现都必须遵循以下约定:

  • 自反性(Reflexive): x.Equals(x) 必须返回 true
  • 对称性(Symmetric): x.Equals(y) 为 true 时,y.Equals(x) 也为 true
  • 传递性(Transitive): 对于任何非 null 的应用值 x, y 和 z,如果 x.Equals(y) 返回 true,并且 y.Equals(z) 也返回true,那么 x.Equals(z) 必须返回 true
  • 一致性(Consistence): 如果多次将对象与另一个对象比较,结果始终相同。只要未修改 x 和 y 的应用对象,x.Equals(y) 连续调用 x.Equals(y) 返回相同的值
  • 非null(Non-null): 如果 x 不是 null,y 为 null,则 x.Equals(y) 必须为 false

GetHashCode:

  • 两个相等对象根据 equals 方法比较时相等,那么这两个对象中任意一个对象的 GetHashCode 方法都必须产生同样的整数
  • 在我们未对对象进行修改时,多次调用 GetHashCode 使用返回同一个整数。在同一个应用程序中多次执行,每次执行返回的整数可以不一致
  • 如果两个对象根据 Equals 方法比较不相等时,那么调用这两个对象中任意一个对象的 GetHashCode 方法,不一同的整数。但不同的对象,产生不同整数,有可能提高散列表的性能

以上内容摘自@冯辉 的博客: https://www.cnblogs.com/yyfh/p/12245916.html

默认情况

我们定义一个如下的 Class:

public class Example
{
	public string? Str { get; set; }
	public int Number { get; set; }
}

在默认情况下进行比较,我们会得到如下结果:

var e1 = new Example { Str = "Str", Number = 0 };
var e2 = new Example { Str = "Str", Number = 0 };
var e3 = e2;
Console.WriteLine(e1.Equals(e1)); //True
Console.WriteLine(e1.Equals(e2)); //False
Console.WriteLine(e3.Equals(e2)); //True

var hashSet = new HashSet<Example>();
Console.WriteLine(hashSet.Add(e1)); //True
Console.WriteLine(hashSet.Add(e1)); //False
Console.WriteLine(hashSet.Add(e2)); //True
Console.WriteLine(hashSet.Add(e3)); //False

重写 Equals 方法

如果我们需要让 Str 字段和 Number 字段相等时,就判断两个 Example 类相等,就要对 Equals 方法进行重写(override):

public class Example
{
    public string? Str { get; set; }
    public int Number { get; set; }

    public override bool Equals(object? obj)
    {
        if (obj is not Example example) return false;
        return this.Number.Equals(example.Number) && string.Equals(this.Str, example.Str);
    }
}

这时再运行上面的比较:

var e1 = new Example { Str = "Str", Number = 0 };
var e2 = new Example { Str = "Str", Number = 0 };
var e3 = e2;
Console.WriteLine(e1.Equals(e1)); //True
Console.WriteLine(e1.Equals(e2)); //True
Console.WriteLine(e3.Equals(e2)); //True

var hashSet = new HashSet<Example>();
Console.WriteLine(hashSet.Add(e1)); //True
Console.WriteLine(hashSet.Add(e1)); //False
Console.WriteLine(hashSet.Add(e2)); //True
Console.WriteLine(hashSet.Add(e3)); //False

重写 GetHashCode 方法

现在e1.Equals(e2)的结果已经是True了,但是 HashSet 在添加了 e1 之后,依然可以添加 e2;并且你会发现编译器发出了一个警告:'Example' overrides Object.Equals(object o) but does not override Object.GetHashCode()。所以我们还需要重写 GetHashCode 方法。

在 Example 类中添加一个 GetHashCode 重写:

public override int GetHashCode()
{
    if (this.Str == null)
        return 0;
    return Str.GetHashCode() * Number.GetHashCode();
}

输出就会变成:

var hashSet = new HashSet<Example>();
Console.WriteLine(hashSet.Add(e1)); //True
Console.WriteLine(hashSet.Add(e1)); //False
Console.WriteLine(hashSet.Add(e2)); //False
Console.WriteLine(hashSet.Add(e3)); //False

有需要的话还可以重载(overload)== 操作符,实现 e1 == e2True