views:

130

answers:

3

I would like to be able to compare two classes derived from the same abstract class in C#. The following code illustrates my problem.

I now I could fix the code by making BaseClass non abstract and then return a new BaseClass object in ToBassClass(). But isn't there a more elegant and efficient solution?

abstract class BaseClass
{
   BaseClass(int x)
   {
       X = x;
   }

   int X { get; private set; }

   // It is probably not necessary to override Equals for such a simple class,
   // but I've done it to illustrate my point.
   override Equals(object other)
   {
       if (!other is BaseClass)
       {
           return false;
       }

       BaseClass otherBaseClass = (BaseClass)other;

       return (otherBaseClass.X == this.X);
   }

   BaseClass ToBaseClass()
   {
       // The explicit is only included for clarity.
       return (BaseClass)this;
   }
}

class ClassA : BaseClass
{
   ClassA(int x, int y)
       : base (x)
   {
       Y = y;
   }

   int Y { get; private set; }
}

class ClassB : BaseClass
{
   ClassB(int x, int z)
       : base (x)
   {
       Z = z;
   }

   int Z { get; private set; }
}

var a = new A(1, 2);
var b = new B(1, 3);

// This fails because despite the call to ToBaseClass(), a and b are treated
// as ClassA and ClassB classes so the overridden Equals() is never called.
Assert.AreEqual(a.ToBaseClass(), b.ToBaseClass());
+2  A: 

Well, as Freed pointed out it's a bit odd to use Assert.True here - did you mean Assert.AreEqual? If so, I'd have expected this to work (even without the ToBaseClass call), although it would depend on the test framework.

Equality is tricky when it comes to inheritance though. Personally, I'd create an appropriate IEqualityComparer<BaseClass> which explicitly says "I'm going to test this specific aspect of an object" - which means that inheritance basically doesn't get involved.

Jon Skeet
+2  A: 

It depends on where exactly you want to test for equality. Clearly ClassA and ClassB instances will never be "equal" in the real sense of that word, so overriding Equals to behave like this might actually cause some weird bugs in your code.

But if you want to compare them based on a specific criteria, then you can implement a specific IEqualityComparer (or several comparers) which suites your needs.

So, in this case you would have:

/// <Summary>
/// Compares two classes based only on the value of their X property.
/// </Summary>
public class ComparerByX : IEqualityComparer<BaseClass>
{
     #region IEqualityComparer<BaseClass> Members

     public bool Equals(BaseClass a, BaseClass b)
     {
         return (a.X == b.X);
     }

     public int GetHashCode(BaseClass obj)
     {
         return obj.X.GetHashCode();
     }

     #endregion

}

[Edit] Regarding comment:

Note that this doesn't have anything with overriding the Equals method.

But you will be able to check for equality like this:

IEqualityComparer<BaseClass> comparer = new ComparerByX();
Assert.True(comparer.Equals(a, b));

This may not seem like a great thing at first, but it gives you several advantages:

a) You can have as many IEqualityComparer<T> implementations as you want. Depending on the case, it may turn up that you Equals override is not so great after all. Then you risk breaking all of your code depending on this.

b) There are actually many classes which use IEqualityComparer<T> to compare items.

For example, you might want to use the BaseClass as a key in a dictionary. In that case, you would use the Dictionary<Key,Value> constructor overload which accepts an IEqualityComparer<T>:

Dictionary<BaseClass, SomeOtherClass> dictionary 
    = new Dictionary<BaseClass, SomeOtherClass>(new ComparerByX());

This way, dictionary will use the custom ComparerByX during key lookup.

Also, for example, if you are using LINQ, you can check the Distinct() method example. It also supports an overload which returns distinct values, but compared using the specified custom IEqualityComparer.

Groo
Will this make a.ToBaseClass() == b.ToBaseClass(), and still keep a != b?
Jan Aagaard
+2  A: 

First, your code doesn't compile. Second, when your code is fixed so that it does compile (in particular, Assert.True is changed to Assert.AreEqual), I see the results that you are expecting. And that's a good thing as that's the correct behavior. But you can't rely on inheritors not overriding Object.Equals so if you want comparison to go by the base class only then you should implement IEqualityComparer<BaseClass>.

Here's a version of your code as you probably intended it so that it does compile:

abstract class BaseClass {
    public BaseClass(int x) { X = x; }

    public int X { get; private set; }

    public override bool  Equals(object other) {
        if (!(other is BaseClass)) {
            return false; 
        }

        BaseClass otherBaseClass = (BaseClass)other;
        return (otherBaseClass.X == this.X);
    }

    public BaseClass ToBaseClass() {
        return (BaseClass)this;
    }
}

class ClassA : BaseClass {
    public ClassA(int x, int y) : base (x) {
        Y = y;
    }

    public int Y { get; private set; }
}

class ClassB : BaseClass {
    public ClassB(int x, int z) : base (x) {
        Z = z;
    }

    public int Z { get; private set; }
}

class Program {
    static void Main(string[] args) {
        var a = new ClassA(1, 2);
        var b = new ClassB(1, 3);
        Assert.AreEqual(a.ToBaseClass(), b.ToBaseClass());
    }
}
Jason