Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Implementing IEquatable Properly

4.76/5 (27 votes)
21 Sep 2007CPOL5 min read 1   506  
Explains how to properly implement the IEquatable interface.

Introduction

In this article, I will show you what the IEquatable interface is and how to implement it properly. I'll also show you why it is important to override the Object.Equals method when implementing the interface.

The IEquatable Interface

To check the equality between two classes, the System.Object class contains two Equals methods with the following definitions:

C#
public virtual bool Equals(object obj)
public static bool Equals(object objA, object objB)

These Equals methods are used to check the equality between two objects. The static method exists to be able to work with null values. If objA is null and you would perform objA.Equals(objB), you will get a NullReferenceException. Therefore, the static method performs additional null checks. Below are the steps that are performed when using the static method.

  • check if objA and objB are identical: returns true if both objects refer to the same memory location or both objects are null
  • check if objA or objB is null: returns false if either one of the objects is null
  • compare the values of objA and objB by using the objA.Equals(objB) method

If you want to have your own implementation, you can override this method. Since the Equals method has a parameter of type Object, a cast will be needed in order to be able to access class specific members.

This is where the IEquatable interface comes in. The IEquatable is a new generic interface in .NET 2.0 that allows you to do the same as the System.Object.Equals method but without having to perform casts. So, when implementing this interface, you can reduce the number of casts, which is a good thing for performance. Especially when working a lot with generic collections, since generic collections make use of this equality comparison in some of their methods (List<T>.Equals(), List<T>.IndexOf(), List<T>.LastIndexOf(), ...).

Note: Since value types are not Objects and the Equals methods require Objects as parameters, boxing and unboxing will occur when performing an equality check. See paragraph "Value Types" on how to avoid this.

The Problem

A lot of people don't override the object.Equals method and only implement the IEquatable.Equals method. At first, nothing seems wrong with that, but when you look further, you will see that you can have strange results. Depending on the way you check the equality between two objects, the .NET runtime can use different paths to check the equality of both objects.

If you don't implement the interface, the runtime will use the Object.Equals method. When you implement the IEquatable interface, the runtime will use the IEquatable.Equals method only if you pass an object with the same type as defined in the interface implementation; otherwise, it will use the Object.Equals method.

To make it easier to understand, I've created a sample application that shows you when the runtime uses which path to check the equality between two objects.

First, I create a class named Employee which implements the IEquatable interface. This class contains two properties: Name and Account. Just as an example, I consider two instances of the Employee class to be the same from the moment that they have the same value for the Account property (independent of their Name property). This is implemented in the Equals method, which you need to create when implementing the IEquatable interface.

C#
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));
    }
}

In a console application, I then create two instances of the Employee class and give them the same values for their Account property and different values for their Name property. I will then check for equality in different ways:

C#
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();
    }
}

Output:

IEquatable1.jpg

Explanation

  • Scenario 1: Comparing with an object of the same class
  • In the first scenario, I simply use the Employee.Equals method. If you use the debugger, you can see that the interface is used by executing the Equals method which will return true.

  • Scenario 2: Comparing with an object from a different class
  • In the second scenario, I first do a cast of the Employee object and assign it to a new instance of the Object class. If you then use the debugger, you will see that it doesn't enter the Equals method.

    This is because, at this moment, the runtime is using the Equals method from the Object class. Since the Object.Equals method doesn't contain our custom validation rules, the result will be false.

  • Scenario 3: Using an ArrayList.Contains method to look for an object in the list (analog to 2)
  • In the third scenario, I use the ArrayList.Contains method. If you look at the method definition, you can see that it uses an Object as parameter.

    IEquatable2.jpg

    Because of this, the ArrayList.Contains method uses the same logic as in the second scenario by using the Object.Equals, and will return false.

The Solution

To avoid having different results depending on the way you check the equality of the objects, you should override the Object.Equals method in your Employee class. This ensures that whenever a class doesn't use the IEquatable.Equals method, still the same checks will be performed. When overriding the Object.Equals, it is a best practice to also override the Object.GetHashCode method. You can do this by adding the following code to the Employee class:

C#
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;
    }
}

If you now run the application again, you will see that for scenario 2 and 3, the overridden Equals method will be used and that all scenarios will return true.

Value Types

When implementing the IEquatable interface on value types, you also need to overload the equality (==) and inequality (!=) operators. This is important to avoid boxing and unboxing when doing an equality check. You can implement this by adding the following code to the Employee class:

C#
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);
}

History

  • 21 September 2007: Original article.
  • 25 September 2007:
    • Added the "The IEquatable Interface" paragraph.
    • Changed the IEquatable.Equals method to work with null values.
    • Changed the operator == and operator != implementation to work with null values.
    • Updated the demo code to reflect the changes in the document.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)