C# IEquatable和IEqualityComparer 最佳实践
前言
IEquatable<T>
和 IEqualityComparer<T>
是 C#
中用于比较对象的接口,它们有以下区别:
- IEquatable
IEquatable<T>
是一个泛型接口,定义了一个用于比较对象相等性的方法Equals(T other)
。
当你想要在类中自定义相等性比较的逻辑时,可以实现IEquatable<T>
接口。
实现了IEquatable<T>
接口的类可以通过调用Equals
方法来进行对象之间的相等性比较。 - IEqualityComparer
IEqualityComparer<T>
也是一个泛型接口,定义了两个方法:Equals(T x, T y)
和GetHashCode(T obj)
。
当你需要自定义对象之间的相等性比较逻辑和哈希码计算逻辑时,可以实现IEqualityComparer<T>
接口。
通常场景他们使用效果是一样的,例如进行Enumerable.Distinct
IEquatable<T>
IEquatable<T> 是 C#
中用于比较对象相等性的泛型接口。它定义了一个名为 Equals
的方法,用于比较对象与另一个对象的相等性。
通过实现 IEquatable<T>
接口,你可以在类中自定义对象相等性的比较逻辑。这使得你的类更具灵活性,可以根据你的需求来确定两个对象是否相等。
根据MS
推荐实现了Equals
最好也实现他的操作符==
和!=
using System;
using System.Collections.Generic;
using System.Xml.Linq;
static class Example
{
static void Main()
{
List<Person> personList = new List<Person>()
{
new Person() { Name = "A", Age = 1, },
new Person() { Name = "B", Age = 2, },
new Person() { Name = "B", Age = 2, },
new Person() { Name = "C", Age = 3, },
new Person() { Name = "C", Age = 3, },
};
IEnumerable<Person> nameDisList = personList.Distinct(); // 实现了 IEquatable 按照 Name + Age 进行去重
// {Name=A, Age=1},{Name=B, Age=2},{Name=C, Age=3}
Console.WriteLine(string.Join(",", nameDisList));
Console.ReadKey();
}
}
public class Person : IEquatable<Person>
{
public int Age
{
get;
set;
}
public string Name
{
get;
set;
}
public override bool Equals(object obj)
{
return this.Equals(obj as Person); // 调用内部的对比即可
}
public bool Equals(Person other)
{
if (other == null)
return false;
// Optimization for a common success case.
if (ReferenceEquals(this, other))
return true;
if (this.Age == other.Age && this.Name == other.Name)
return true;
else
return false;
}
public override int GetHashCode()
{
// 选择两个不同的质数,例如 17 和 23
// 关于质数,可以参考 https://en.wikipedia.org/wiki/Prime_number
// 重写 GetHashCode() 方法最佳实践请见 https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode
int hashCode = 17;
hashCode = hashCode * 23 + EqualityComparer<string>.Default.GetHashCode(this.Name);
hashCode = hashCode * 23 + EqualityComparer<int>.Default.GetHashCode(this.Age);
return hashCode;
}
public static bool operator ==(Person p1, Person p2)
{
if (p1 is null && p2 is null) // 一定不要操作符重写中使用 == 操作符来判断两个对象是否相等,否则会堆栈溢出
return true;
if (((object)p1) == null || ((object)p2) == null)
return Equals(p1, p2);
return p1.Equals(p2);
}
public static bool operator !=(Person p1, Person p2)
{
return !(p1 == p2); // 使用不等于即可
}
}
IEqualityComparer<T>
IEqualityComparer<T> 是 C#
中用于比较对象相等性的泛型接口。它定义了两个方法:Equals(object x, object y)
和 GetHashCode(object obj)
。
Equals(object x, object y)
方法用于比较两个对象 x
和 y
是否相等。它返回一个布尔值,指示两个对象是否相等。
GetHashCode(object obj)
方法用于计算对象 obj
的哈希码。哈希码是一个整数,用于在哈希表等数据结构中进行快速查找和比较。
通常情况下,IEqualityComparer
接口用于在没有泛型的情况下,提供自定义的对象相等性比较和哈希码计算逻辑。 你可以实现 IEqualityComparer
接口来定义你自己的比较器,以便在需要自定义对象相等性比较的情况下使用。
using System;
using System.Collections.Generic;
using System.Xml.Linq;
static class Example
{
static void Main()
{
List<Person> personList = new List<Person>()
{
new Person() { Name = "A", Age = 1, },
new Person() { Name = "B", Age = 2, },
new Person() { Name = "B", Age = 3, },
new Person() { Name = "C", Age = 4, },
new Person() { Name = "B", Age = 4, },
};
IEnumerable<Person> nameDisList = personList.Distinct(new PersonNameEqualityComparer());
IEnumerable<Person> ageDisList = personList.Distinct(new PersonAgeEqualityComparer());
// {Name=A, Age=1},{Name=B, Age=2},{Name=C, Age=4}
Console.WriteLine(string.Join(",", nameDisList));
// {Name = A, Age = 1},{ Name = B, Age = 2},{ Name = B, Age = 3},{ Name = C, Age = 4}
Console.WriteLine(string.Join(",", ageDisList));
Console.ReadKey();
}
}
class PersonNameEqualityComparer : IEqualityComparer<Person>
{
public bool Equals(Person p1, Person p2)
{
if (ReferenceEquals(p1, p2))
return true;
if (p2 is null || p1 is null)
return false;
return p1.Name == p2.Name;
}
public int GetHashCode(Person p) => p.Name.GetHashCode();
}
class PersonAgeEqualityComparer : IEqualityComparer<Person>
{
public bool Equals(Person p1, Person p2)
{
if (ReferenceEquals(p1, p2))
return true;
if (p2 is null || p1 is null)
return false;
return p1.Age == p2.Age;
}
public int GetHashCode(Person p) => p.Age.GetHashCode();
}
MS
不建议直接派生IEqualityComparer<T>
,而是使用EqualityComparer<T>
,本例代码可以直接改成EqualityComparer
建议从 EqualityComparer
类派生,而不是实现 IEqualityComparer 接口,因为 EqualityComparer 类使用 IEquatable .Equals 方法而不是 Object.Equals 方法测试相等性。 这与 Contains类和其他泛型集合的 Dictionary<TKey,TValue> 、IndexOf、 LastIndexOf和 Remove 方法一致。
如果是单纯的去重,高版本的C#
可以使用Enumerable.DistinctBy
后言
通过上面介绍我们了解了它们两种的使用方式,那么什么时候用IEqualityComparer<T>
什么时候用IEquatable<T>
呢?
- 当目标类你没有源代码,无法直接修改的时候请使用
IEqualityComparer<T>
- 当目标类你只需要一种对比方式可以考虑使用
IEquatable<T>
- 当目标类有多重对比方式,例如根据
A
和B
或者B
和C
等多重属性进行对比,你可以定义多个IEqualityComparer<T>
- 当然两种方式可以共存,根据需要使用
关于 HashCode
对于GetHashCode
方法来说我们即使返回的是不同的值,但是也有可能会出现哈希冲突,所以我们需要尽量减少哈希冲突的概率,这样可以提高哈希表的性能。
即使你返回相同的值,最终效果也是一样的,因为它都会走一遍对比逻辑,性能会低很多。
对于高版本可以使用ValueTuple,值元祖不会在堆上创建垃圾对象。
低版本可以使用匿名类型,使用匿名类型的原始技术会在堆上创建一个对象,也就是垃圾,因为匿名类型是作为类实现的。
// ValueTuple
(PropA, PropB, PropC, PropD).GetHashCode();
// Anonymous Type
new { PropA, PropB, PropC, PropD }.GetHashCode();
HashCode最佳实践可以参考stackoverflow这篇文章。