Be careful when you override Equals (and use IEquatable)

Hi!

Please, bare with me through this complex intro to the real goodies below! ;~)

In a project I'm working on we decided we wanted a single instance cache so we'd not fetch the same data form the data store over and over again. We started adding weak references of instances to a dictionary. Worked nicely.

We then added the functionality of lazy loading in our code. At times we needed the data in class A only and not all the data in the subclass B. The key for the instance of A then needed to be the same as the key for the instance of B in our case. No problem there either. Actually this became a nice dynamic solution. Lovely!

Then what's the problem pray tell? Well say I input A in the cache and later request the B instance which is based on A. What do I find in my cache? A! OK so the cache must also compare the types of A and B when it is updated, right? Right! This is pretty tricky business I tell you!

If I request B and the cache holds A I get a cache hit if I'm not careful. Also if I have A in the cache and try to add B it did not get added because it was already there. The result was that B never got cached! Got you sufficiently confused yet? Even I lost my train of thought there for a while! ;~)

All this mess comes from correctly overriding Equals on the objects A and B.

Now for the goodies promised! (complex intro over)

First a question: What does it mean to be equal?

There are at least two scenarios here: B is a subclass of A. A.equals(B) can be either true or false depending on your specific needs. In our case we don't want them to be equal but I can surely see that in some cases you'd want them to be. Let's leave that later example and focus on the former.

Now for a tip: Use the System.IEquatable<T> interface! With this you can get a typed Equals method for each type T you implement. This is nice since in typed comparisons you will jump right passed the Equals(object) method and into the typed version thus avoiding runtime casts. Sweet!

OK so far so good. Now for some code!

  • I have three classes A, B and C which have an inheritance chain: C inherits B which inherits A.
  • Each has a pointless field to show you that the Equals does compare state for each object type.
  • All three classes override Equals(object) and implement IEquatable<T> where T is the type of itself, for instance A: IEquatable<A>.
  • Each Equals implementation with a parent calls it's base.Equals method thus I only need to implement equality on the same object level once.
  • I have nine tests which are all green.
  • Each test shows to which version of Equality the call gets forwarded!

Copy paste this code and test it if you like. Post continues below!

public class A: IEquatable<A>
{
    public int a;

    public override bool Equals(object obj)
    {
        A aObj = obj as A;
        return aObj != null && Equals(aObj);
    }

    public bool Equals(A other)
    {
        return other != null &&
         (GetType().Equals(other.GetType())) &&
         a == other.a;
    }
}

public class B: A, IEquatable<B>
{
    public int b;

    public override bool Equals(object obj)
    {
        B bObj = obj as B;
        return bObj != null && Equals(bObj);
    }

    public bool Equals(B other)
    {
        return other != null &&
         b == other.b &&
         base.Equals(other);
    }
}

public class C : B, IEquatable<C>
{
    public int c;

    public override bool Equals(object obj)
    {
        C cObj = obj as C;
        return cObj != null && Equals(cObj);
    }

    public bool Equals(C other)
    {
        return other != null &&
                 c == other.c &&
                 base.Equals(other);
    }
}

[TestFixture]
public class EquatableTests
{
    [Test]
    public void AEqualsA()
    {
        // Call goes to A.Equals(A)
        Assert.IsTrue(new A().Equals(new A()));
    }

    [Test]
    public void AEqualsAObject()
    {
        object a = new A();
        // Call goes to A.Equals(object)
        Assert.IsTrue(new A().Equals(a));
    }

    [Test]
    public void ADoesNotEqualNull()
    {
        // Call goes to A.Equals(A)
        Assert.IsFalse(new A().Equals(null), "This will call the typed Equals(A) which will result in a null ref exception if you're not careful!");
    }

    [Test]
    public void ADoesNotEqualNull2()
    {
        A a = null;
        // Call goes to A.Equals(A)
        Assert.IsFalse(new A().Equals(a), "This will call the typed Equals(A) which will result in a null ref exception if you're not careful!");
    }

    [Test]
    public void ADoesNotEqualNull3()
    {
        object o = null;
        // Call goes to A.Equals(object)
        Assert.IsFalse(new A().Equals(o));
    }

    [Test]
    public void ADoesNotEqualB()
    {
        A a = new A();
        B b = new B();
        // Call goes to A.Equals(A)
        Assert.IsFalse(a.Equals(b));
    }

    [Test]
    public void BDoesNotEqualA()
    {
        A a = new A();
        B b = new B();
        // Call goes to A.Equals(A)
        Assert.IsFalse(b.Equals(a));
    }

    [Test]
    public void BEqualsBObject()
    {
        object b = new B();
        // Call goes to B.Equals(object)
        Assert.IsTrue(new B().Equals(b));
    }

    [Test]
    public void CDoesNotEqualB()
    {
        B b = new B();
        C c = new C();
        // Call goes to B.Equals(B)
        Assert.IsFalse(c.Equals(b));
    }
}

A few comments on the code:

  • The Equals(object) implementation is the simplest possible. Runtime cast the object and if it is not null call the typed comparison! If you don't check for null here you'll get an infinite loop since the next call would go back to Equals(object).
  • The real comparison of the objects takes place in Equals(A), Equals(B) and Equals(C).
  • Calls get forwarded to the base.Equals() method which will enter the typed version of the parent. Nice! Only runtime cast once when we really don't know the incoming type.
  • Why do I need to override Equals on each subtype? Answer: Otherwise I will not enter the subtypes typed Equals method but rather go to Equals(A) directly. This will mean that C.Equals((object)new C()) will not call Equals(C)! Thus we need the Equals override on each subtype. Annoying but true.

Finally in the Equals(A) implementation I compare the types of the objects at hand.

Now for the million dollar question: Which tests 1..9 will fail if I comment out the type comparing line in Equals(A):

    public bool Equals(A other)
    {
        return other != null &&
              // (GetType().Equals(other.GetType())) &&
              a == other.a;
    }

Answer: The three tests: ADoesNotEqualB, BDoesNotEqualA and CDoesNotEqualB. Reason is that A.Equals(A) will not care if the incoming call is a subtype (B or C) it will treat it as if it were an A.

Like is said earlier: In our case it makes sense for A.Equals(B) to come out false. In your case perhaps they should be equal if the "A part" of B contains the same data as the other instance of A!

In conclusion:

  1. Figure out what Equals means for your application!
  2. Use IEquatable<T>,: It makes the implementations nicer!
  3. Also use base.Equals() because that will make sense to implement equals for each type in only one place!
  4. Be careful! This is tricky business and if you do it wrong you'll not get the behavior you re seeking!

Cheers,

/Magnus

posted @ Tuesday, December 12, 2006 5:30 PM

Print

Comments on this entry:

No comments posted yet.

Your comment:



 (will not be displayed)


 
 
 
Please add 5 and 8 and type the answer here:
 

Live Comment Preview: