[Journal - Implementing Equals]

Implementing Equals

Thursday, January 5, 2006

One of the rules for implementing the Equals() method is that that it should be reflexive, that is, if object a considers itself equal to object b, then object b should deem itself equal to a in reverse. And vice versa.

Another rule is that for two objects to be equal, their type must be exactly the same. To achieve that, it is not sufficient to override Equals() at every level in the class hierarchy: you have to be prepared to compare yourself against an object of a more derived type. Even for that latter type, an special Equals() implementation might not even be needed, if that type does not add any information that's critical for determining equality. Therefore, the objects' types need to be compared (System.Type.Equals() will take care of that); it is not enough to use C#'s is operator or the like.

There are some other rules, but I think these are the most basic ones. Another issue is how to deal with null references (never equal to any object).

To cut a long story short, here's a wrapper I use implementing these rules, which also makes the inevitable casting business a bit easier:

// Gregor.Core
public delegate bool EqualsCallback<T>(T a, T b)

// Gregor.Core.Flow
public static bool AreEqual<T>(T self, object obj, EqualsCallback<T> cb){
    if(((object)self) == obj){
        // identical or both null
        return true;
    }else if(null == self || null == obj){
        // only one is null
        return false;
    }else if(self.GetType().Equals(obj.GetType())){
        // exact same type
        return cb(self, (T)obj);
    }else{
        // not exact same type
        return false;
    }
}

Here's how it can be used:

public override bool Equals(object obj){
    return Flow.AreEqual<Person>(this, obj, delegate(Person a, Person b){
        return 0 == string.Compare(a.Name, b.Name, true);
    };
}

The callback, here handily implemented locally as an anonymous method, is only invoked if there are no null references, and the types are the very same.

Remember to override GetHashCode() as well when overriding Equals().