65.9K
CodeProject 正在变化。 阅读更多。
Home

正确实现IEquatable

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (25投票s)

2007年9月21日

CPOL

5分钟阅读

viewsIcon

183954

downloadIcon

527

解释如何正确实现 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 值。如果 objAnull,并且您执行 objA.Equals(objB),您将得到一个 NullReferenceException。因此,static 方法会执行额外的 null 检查。以下是使用 static 方法时执行的步骤。

  • 检查 objAobjB 是否相同:如果两个对象引用相同的内存位置 **或** 两个对象都是 null,则返回 true
  • 检查 objAobjB 是否为 null:如果其中任何一个对象为 null,则返回 false
  • 使用 objA.Equals(objB) 方法比较 objAobjB 的值

如果您想拥有自己的实现,可以覆盖此方法。由于 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 接口。这个类包含两个属性:NameAccount。举例来说,我将 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();
    }
}

输出

IEquatable1.jpg

解释

  • 场景 1:与同一类的对象进行比较
  • 在第一个场景中,我简单地使用了 Employee.Equals 方法。如果您使用调试器,您可以看到通过执行 Equals 方法(将返回 true),接口被使用了。

  • 场景 2:与不同类的对象进行比较
  • 在第二个场景中,我首先对 Employee 对象进行强制转换,并将其赋值给 Object 类的新实例。如果您随后使用调试器,您将看到它没有进入 Equals 方法。

    这是因为,此时,运行时正在使用 Object 类中的 Equals 方法。由于 Object.Equals 方法不包含我们的自定义验证规则,结果将为 false

  • 场景 3:使用 ArrayList.Contains 方法在列表中查找对象(类似于场景 2)
  • 在第三个场景中,我使用 ArrayList.Contains 方法。如果你查看方法定义,你会发现它使用一个 Object 作为参数。

    IEquatable2.jpg

    因此,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 值。
    • 更新了演示代码以反映文档中的更改。
© . All rights reserved.