正确实现IEquatable






4.76/5 (25投票s)
解释如何正确实现 IEquatable 接口。
引言
本文将向您展示 IEquatable
接口是什么以及如何正确实现它。我还会向您展示为什么在实现接口时覆盖 Object.Equals
方法很重要。
IEquatable 接口
为了检查两个类之间的相等性,System.Object
类包含两个 Equals
方法,其定义如下:
public virtual bool Equals(object obj)
public static bool Equals(object objA, object objB)
这些 Equals
方法用于检查两个对象之间的相等性。
方法的存在是为了能够处理 static
值。如果 null
objA
是
,并且您执行 null
objA.Equals(objB)
,您将得到一个 NullReferenceException
。因此,
方法会执行额外的 static
检查。以下是使用 null
方法时执行的步骤。static
- 检查
objA
和objB
是否相同:如果两个对象引用相同的内存位置 **或** 两个对象都是
,则返回null
true
- 检查
objA
或objB
是否为
:如果其中任何一个对象为null
,则返回null
false
- 使用
objA.Equals(objB)
方法比较objA
和objB
的值
如果您想拥有自己的实现,可以覆盖此方法。由于 Equals
方法有一个类型为 Object
的参数,因此需要进行强制转换才能访问类特定的成员。
这就是 IEquatable
接口的用武之地。IEquatable
是 .NET 2.0 中一个新的泛型接口,它允许您执行与 System.Object.Equals
方法相同的操作,但无需执行强制转换。因此,在实现此接口时,您可以减少强制转换的次数,这对于性能而言是一件好事。尤其是在大量使用泛型集合时,因为泛型集合在其某些方法中(List<T>.Equals()
、List<T>.IndexOf()
、List<T>.LastIndexOf()
等)使用了这种相等性比较。
注意:由于值类型不是 Object
,并且 Equals
方法要求 Object
作为参数,因此在执行相等性检查时会发生装箱和拆箱。请参阅“值类型”段落,了解如何避免这种情况。
问题
很多人不覆盖 object.Equals
方法,而只实现 IEquatable.Equals
方法。乍一看似乎没什么问题,但深入研究后,你会发现可能会出现奇怪的结果。根据您检查两个对象相等性的方式,.NET 运行时可以使用不同的路径来检查两个对象的相等性。
如果您不实现该接口,运行时将使用 Object.Equals
方法。当您实现 IEquatable
接口时,运行时将 **仅** 在您传入与接口实现中定义的 **相同类型** 的对象时使用 IEquatable.Equals
方法;否则,它将使用 Object.Equals
方法。
为了更容易理解,我创建了一个示例应用程序,向您展示运行时何时使用哪种路径来检查两个对象之间的相等性。
首先,我创建了一个名为 Employee
的类,它实现了 IEquatable
接口。这个类包含两个属性:Name
和 Account
。举例来说,我将 Employee
类的两个实例视为相同,只要它们的 Account
属性具有相同的值(与它们的 Name
属性无关)。这是在 Equals
方法中实现的,您在实现 IEquatable
接口时需要创建该方法。
public class Employee : IEquatable<Employee>
{
public string Account;
public string Name;
public Employee(string account, string name)
{
this.Account = account;
this.Name = name;
}
public bool Equals(Employee other)
{
if (other == null) return false;
return (this.Account.Equals(other.Account));
}
}
在一个控制台应用程序中,我然后创建了 Employee
类的两个实例,并为它们的 Account
属性赋予相同的值,为它们的 Name
属性赋予不同的值。然后我将以不同的方式检查相等性
class Program
{
public static void Main()
{
Employee emp1 = new Employee("GEVE", "Geert Verhoeven");
ArrayList employees = new ArrayList();
employees.Add(emp1);
Employee emp2 = new Employee("GEVE", "Geert");
// SCENARIO 1: Comparing with an object from the same class.
Console.WriteLine("emp1.Equals(emp2): {0}", emp1.Equals(emp2));
// SCENARIO 2: Comparing with an object from a different class.
Object obj = emp2;
Console.WriteLine("emp1.Equals(obj): {0}", emp1.Equals(obj));
// SCENARIO 3: Using an ArrayList (analog to SCENARIO 2)
Console.WriteLine("employees.Contains(emp2): {0}",
employees.Contains(emp2));
Console.ReadLine();
}
}
输出
解释
- 场景 1:与同一类的对象进行比较
- 场景 2:与不同类的对象进行比较
- 场景 3:使用
ArrayList.Contains
方法在列表中查找对象(类似于场景 2)
在第一个场景中,我简单地使用了 Employee.Equals
方法。如果您使用调试器,您可以看到通过执行 Equals
方法(将返回
),接口被使用了。true
在第二个场景中,我首先对 Employee
对象进行强制转换,并将其赋值给 Object
类的新实例。如果您随后使用调试器,您将看到它没有进入 Equals
方法。
这是因为,此时,运行时正在使用 Object
类中的 Equals
方法。由于 Object.Equals
方法不包含我们的自定义验证规则,结果将为
。false
在第三个场景中,我使用 ArrayList.Contains
方法。如果你查看方法定义,你会发现它使用一个 Object
作为参数。
因此,ArrayList.Contains
方法使用与第二个场景相同的逻辑,即使用 Object.Equals
,并将返回
。false
解决方案
为了避免根据检查对象相等性的方式获得不同的结果,您应该在 Employee
类中覆盖 Object.Equals
方法。这可以确保当一个类不使用 IEquatable.Equals
方法时,仍然会执行相同的检查。在覆盖 Object.Equals
时,最佳实践是同时覆盖 Object.GetHashCode
方法。您可以通过将以下代码添加到 Employee
类来完成此操作
public override int GetHashCode()
{
return this.Account.GetHashCode();
}
public override bool Equals(object obj)
{
Employee emp = obj as Employee;
if (emp != null)
{
return Equals(emp);
}
else
{
return false;
}
}
如果您现在再次运行应用程序,您将看到对于场景 2 和 3,将使用重写的 Equals
方法,并且所有场景都将返回
。true
值类型
在值类型上实现 IEquatable
接口时,您还需要重载相等 (==) 和不等 (!=) 运算符。这对于在进行相等性检查时避免装箱和拆箱非常重要。您可以通过将以下代码添加到 Employee
类来实现这一点
public static bool operator ==(Employee emp1, Employee emp2)
{
if (object.ReferenceEquals(emp1, emp2)) return true;
if (object.ReferenceEquals(emp1, null)) return false;
if (object.ReferenceEquals(emp2, null)) return false;
return emp1.Equals(emp2);
}
public static bool operator !=(Employee emp1, Employee emp2)
{
if (object.ReferenceEquals(emp1, emp2)) return false;
if (object.ReferenceEquals(emp1, null)) return true;
if (object.ReferenceEquals(emp2, null)) return true;
return !emp1.Equals(emp2);
}
历史
- 2007 年 9 月 21 日:原创文章。
- 2007 年 9 月 25 日
- 添加了“IEquatable 接口”段落。
- 修改了
IEquatable.Equals
方法以处理
值。null
- 修改了运算符
==
和运算符!=
的实现以处理
值。null
- 更新了演示代码以反映文档中的更改。