Equals() Implementation

Comparing objects in .NET can be a bit confusing. .NET gives us the object base class with its Equals() method, then there are == and != operators that can be overloaded and finally, we have interfaces like IEquatable<>, IComparable<> and IComparer<>.

It also isn’t clearly stated whether these methods are supposed to compare object identity (check if two variables hold the same instance of a class) or object state (check if two variables carry objects with the same values).

This article tries to explain how .NET does comparison and demonstrates a good way to implement the default Equals() that elegantly works for inherited classes and avoids duplicate code.

If You Do Nothing…

For classes (reference types), .NET will do any object identity comparison unless you override the Equals() method. The following code will output “false”:

class MyClass {
  public int X;
  public int Y;
}

class Program {
  static void Main() {
    MyClass first = new MyClass();
    first.X = 12;
    first.Y = 34;
    MyClass second = new MyClass();
    second.X = 12;
    second.Y = 34;

    // Will print false
    Console.WriteLine(first.Equals(second));
  }
}

For structures (value types), an object state comparison will happen instead. Thus, the same code as before with a structure will output “true”:

struct MyStruct {
  public int X;
  public int Y;
}

class Program {
  static void Main() {
    MyStruct first = new MyStruct();
    first.X = 12;
    first.Y = 34;
    MyStruct second = new MyStruct();
    second.X = 12;
    second.Y = 34;

    // Will print true
    Console.WriteLine(first.Equals(second));
  }
}

Because you can always do an object identity comparison using the static object.ReferenceEquals() method, comparison operators should default to comparing an object’s state, not its identity.

In special cases where you actually need an identity comparison, you can do one of the following things, listed in order of preference:

  1. Explicitly use object.ReferenceEquals() when the comparison is in your own code
  2. Use a special IComparer<> for collections or dictionaries
  3. Let Equals() do an object identity comparison and document this behavior

Implementing object.Equals() in a Plain Class

My suggestion to you is to actually implement two Equals() methods:

class MyClass {

  public override bool Equals(object other) {
    return Equals(other as MyClass);
  }

  public virtual bool Equals(MyClass other) {
    if(ReferenceEquals(other, null))
      return false;

    return
      (this.X == other.X) &&
      (this.Y == other.Y);
  }

  public int X;
  public int Y;
  
}

Let’s see: If someone compares an instance of MyClass against an object of a different type, the Equals(object other) method is called, where the as operator will return null since the cast is not valid, effectively doing a null comparison which will always compare as false (not equal).

The specialized Equals() method provides a minor performance gain since we can skip the up- and downcasting otherwise associated with the Equals() method. And since this can never be null for an instance method, we can safely assume that if the comparison object is null the comparison result is false.

If both objects are valid and of the same type, we proceed to compare the non-mutable fields of both instances, doing the actual object state comparison.

Implementing object.Equals() in a Derived Class

This scheme can be easily extended over multiple inheritance levels without repeating any code.

class MyDerivedClass : MyClass {

  public override bool Equals(object other) {
    return Equals(other as MyDerivedClass);
  }

  public override bool Equals(MyClass other) {
    return Equals(other as MyDerivedClass);
  }

  public virtual bool Equals(MyDerivedClass other) {
    return
      base.Equals(other) &&
      (this.Z == other.Z);
  }

  public int Z;

}

It’s a bit hard to see what happens when you compare MyDerivedClass against a null pointer, so let’s follow this code path:

  • MyDerivedClass.Equals(object) is called
  • Which calls MyDerivedClass.Equals(MyDerivedClass)
  • MyClass.Equals(MyClass) is called
  • MyClass.Equals(MyClass) finds the comparison object is null and returns false

You will obtain one additional Equals() overload for each level of inheritance you create. I don’t see this as an issue because, as experience shows, well-designed object models usually do not have more than 2 or 3 levels of inheritance.

The Equality Operators

By default, structs don’t have equality operators (== and !=), so you can’t use these operators on a struct unless it explicitly defines these operators. For classes, the default equality operators do an identity comparison, i.e. will return true if both sides of the comparison refer to the same object and false in any other case.

These can make use of the specialized Equals() operator and are therefore easy to implement:

/// <summary>Checks two segment instances for inequality</summary>
/// <param name="first">First instance to be compared</param>
/// <param name="second">Second instance fo tbe compared</param>
/// <returns>True if the instances differ or exactly one reference is set to null</returns>
public static bool operator !=(MyClass first, MyClass second) {
  return !(first == second);
}

/// <summary>Checks two segment instances for equality</summary>
/// <param name="first">First instance to be compared</param>
/// <param name="second">Second instance fo tbe compared</param>
/// <returns>True if both instances are equal or both references are null</returns>
public static bool operator ==(MyClass first, MyClass second) {
  if(ReferenceEquals(first, null))
    return ReferenceEquals(second, null);

  return first.Equals(second);
}

As you can see, the entire logic has been moved to the == operator to avoid duplicating code lines.

Of course, since the comparison operators are static methods, we now might encounter the case where the left operand is null. This is handled by checking whether the left operand is null and then only returning true if the right operand is also null (so null == null would evaluate as true). If only the right operand is null, this will be handled by the Equals() operator.

Drawbacks

The only drawback of this implementation is that you have to implement the specialized Equals() operator in all derived classes and obtain one more Equals() specialization for each level of inheritance. You might want to accept the performance loss of the up- and redowncast dilemma in some special cases and only implement an Equals(object other) operator there.