Search

Advertising

Home Articles Architecture Implementing Object Comparison
Implementing Object Comparison Print E-mail
Written by Markus Ewald   
Thursday, October 12 2006 18:40
Shout it

kick it on DotNetKicks.com

Comparing objects in .NET can be a bit confusing. .NET gives us the object base class with its Equals(), 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));
  }
}

The default == and != operators are built upon object.Equals(). So you would see the same results if you replaced the call to .Equals() in the above code with first == 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 the cases where you actually need an identity comparison, you can:

  1. Use a special IComparer<> if possible
  2. Use an adapter class
  3. Implement object identity comparison and state this behavior in the documentation

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(MyDerivedClass) is called
  • MyClass.Equals(MyClass) is called (this means the method in the base class, not the override in MyDerivedClass
  • MyClass.Equals(MyClass) finds the comparison object is null and returns false
  • In MyDerivedClass.Equals(MyDerivedClass), the comparison stops with the first line (base.Equals(other)) - see shortcut evaluation.

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 Comparison Operators

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.

 


Joomla Template by Joomlashack