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 开发者_如何学JAVAClassA and ClassB classes so the overridden Equals() is never called.
Assert.AreEqual(a.ToBaseClass(), b.ToBaseClass());
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
.
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.
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());
}
}
I seriously recommend using KellermanSoftware.CompareNetObjects (although I am not the author - it is very flexible, effective and so far, bug free!).
I did the same thing to compare 2 objects based on a base class.
1) Create a "BaseClassComparer" which takes the type you want to use for comparisons:
using System;
using KellermanSoftware.CompareNetObjects;
namespace Compare
{
/// <summary>
/// This allows us to compare objects based on a particular class (ie so we can compare on base classes)
/// </summary>
public class BaseClassComparer : KellermanSoftware.CompareNetObjects.TypeComparers.ClassComparer
{
private readonly Type _compareType;
internal BaseClassComparer(Type compareType, RootComparer rootComparer) : base(rootComparer)
{
_compareType = compareType;
}
public override void CompareType(CompareParms parms)
{
parms.Object1Type = _compareType;
parms.Object2Type = _compareType;
base.CompareType(parms);
}
public override bool IsTypeMatch(Type type1, Type type2)
{
if (((_compareType.IsAssignableFrom(type1)) && (_compareType.IsAssignableFrom(type2)))) {
return true;
} else {
return false;
}
}
}
}
Then, add this class to the Kellerman comparer:
_compare = New CompareLogic
_compare.Config.CustomComparers.Add(New BaseClassComparer(compareType, RootComparerFactory.GetRootComparer()))
And compare away. Only differences between the base type (compareType) will be checked/reported...
精彩评论