And after a long winter break where I did several other things (including putting on some kilos of weight that will be disposed soon) that will materialize in a (hopefully) stream of posts…
Definitions and Scenario Layout
Default implementation of object equality (.Equals(obj)) relies on reference equality for reference types and value quality using reflection for value types. Most of the time we are interested in value equality, meaning that an object a is equal to an object b (a.Equals(b) == true) when the values of their members are equal. As a consequence, we will have to override the default implementation of reference types to suit our needs, and we better override the default implementation for value types if we want to have better performance. I leave the reader the task to check the rules on when and how to implement value equality.
With the advent of generics in .Net 2.0 a new companion was added to Equals(obj), that companion is an interface IEquatable<T>, that adds an override to Equals(T), receiving the generic type as an argument. They both work very well together as in most implementations of value equality the objects have to be of the same type to be equal, and then after casting the state of the object is "compared".
It should be clear by now that if we create a type and that type needs to implement value equality, IEquatable<T> needs to be implemented and Equals(obj) overriden according to the aforementioned rules.
To keep things simple I have implemented a very simple Reference Type that will implement value equality. It has 6 integral properties (again, to keep things simple and don’t be bothered with the presence of nulls) which will reveal their usefulness as we advance in the series.
The implementation reads:
- when non-generic equality method is used, the Property1 will be the one evaluated
- but when the generic the method Equals(MyReferenceType) is used, Property2 will be the one to be evaluated
- the hash of the object is calculated using the Property3.
Default implementation of equality operators operates under the same rules, so if we want to use operators for value equality, we will need to override their implementation.
Nice and dandy, but… Under which circumstances is each method used? I have tried to go through some of the most common scenarios in which equality used the code and proving my assumptions with unit test. Code as it is you can access it inside the code repository providing only a brief explanation in written prose.
But.. When? … Which?
As a general rule, for obvious reasons (uni-directional time vectors :-p) pre-2.0 types would use the non-generic version of Equals(), whilst newer type tend to favor the generic implementation.
- ArrayList.IndexOf(): “old-skool” types that uses Equals(object) in order to check the equality of its members when IndexOf() is invoked
- Array.IndexOf(): This is another old-school type, but received a face-lift in framework 2.0. It turns out that has two overloads of the method, using Equals(object) when invoking the non-generic overload, but uses Equals(T) when invoking the generic overload.
- List<T>.IndexOf(): they generic cousin of ArrayList uses genric Equals(T) to check equality of its members.
- IDictionary.Add(): what happens when we try to add objects to an IDictionary implementor? How do we check that the dictionary doesn’t contain the same key yet? Well, for ListDictionary, for example, it uses Equals(object) to check key objects.
- HashTable.Add(): this old friend of us is also an implementor of IDIctionary operating with non-generic overloads. The implementation will generate a hash to optimize access to the members that it contains, but it will also check for equality, turning out to use both Equals(object) and GetHashCode() (when used with its defaults. Later in the series we’ll see how to modify that default behavior)
- IDictionary.Contains(): not surprisingly it will use Equals(object) to find the sought member
- HashTable.Contains(): as with the Add() method, it will use both Equals(object) and GetHashCode()
- Dictionary<T, K>.Add(): we would expect the generic evolution of IDictionary to operate on generic implementations, and we would be half right, as it also uses GetHashCode() to see if we already have the same key.
- HashSet<T>.Add(): this newcomer uses a combination of Equals(T) and GetHashCode() to check if it should add elements to this set type.
- HashSet<T>.Contains(): also uses Equals(T) and GetHashCode()
More on equality to come…