2

A domain class T can be of type ValueObject<T>:

public class Coordinate: ValueObject<Coordinate>
{ ... }

ValueObject<T> implements the IEquatable interface. I want each concrete implementation of ValueObject<T> to provide implementation for bool Equals(T obj), so I have created it as an abstract method:

public abstract class ValueObject<T> : IEquatable<T>
{
    public abstract bool Equals(T obj);

    public static bool operator ==(ValueObject<T> obj1, ValueObject<T> obj2)
    {
        if (object.ReferenceEquals(obj1, obj2)) return true;
        if (object.ReferenceEquals(obj1, null)) return false;
        if (object.ReferenceEquals(obj2, null)) return false;

        return obj1.Equals(obj2);
    }
}

An Equals implementation in the Coordinate class:

public class Coordinate : ValueObject<Coordinate>
{
    // ...

    public override bool Equals(Coordinate other)
    {
        return (other != null) && (this.Latitude == other.Latitude) && (this.Longitude == other.Longitude);
    }
}

ValueObject<T> provides generic operating overriding for == (and for !=, which is not shown above), which applies to all concrete implementations.

The problem is that when the Equals method is called from either the == override, it calls Object.Equals() and not Coordinate.Equals().

Dave New
  • 38,496
  • 59
  • 215
  • 394

2 Answers2

6

The problem is that when the Equals method is called from either the == override, it calls Object.Equals() and not Coordinate.Equals().

No, the problem is that those things are different. If they are the same, as they should be, then there's no problem.

So make them the same. Don't make the derived class do the wrong thing; force them to do the right thing.

public abstract class ValueObject<T> : IEquatable<T>
{
    // Force the derived class to override these.
    public abstract override bool Equals(object obj);
    public abstract override int GetHashcode(object obj);

    // And then consistently use the overridden method as the implementation.
    public virtual bool Equals(T obj)
    {
        return obj1.Equals((object)obj2);
    }
    public static bool operator ==(ValueObject<T> obj1, ValueObject<T> obj2)
    {
         return obj1.Equals((object)obj2);
    }
    public static bool operator !=(ValueObject<T> obj1, ValueObject<T> obj2)
    {
         return !obj1.Equals((object)obj2);
    }
}
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • This has a cleaner public surface but has less code sharing on the inside. In fact, this version of `ValueObject` almost accomplishes nothing. The signature of `==` is still wrong because it allows any `ValueObject` which is too general. Although the surface is now cleaner than before, this approach is still not viable to be exposed publicly from a library. – usr Mar 15 '14 at 14:47
  • @Eric: I understand my mistake now. Although now, in my concrete implementations, I will have to override a weaker `Equals(object)` which means casting will need to occur (i.e. I will need to cast `object` to `Coordinate` in order to compare properties). My initial attempt was to force a more specific `Equals(T)`. – Dave New Mar 15 '14 at 14:50
  • @usr: Sure, it's still messed up in a bunch of ways. – Eric Lippert Mar 15 '14 at 15:03
  • @davenewza: Well you're going to have to override `Equals(object)` at some point so there is no avoiding the type test. An object has to be comparable to every other object, no getting around it. It's unfortunate. – Eric Lippert Mar 15 '14 at 15:04
  • Your == and != operators have the same formula. – LarsTech Jun 06 '18 at 22:20
  • 1
    @LarsTech: Whoops! Thanks for noticing that. I'll fix it. – Eric Lippert Jun 06 '18 at 23:03
1

When you say obj1.Equals(obj2), obj2 is of type ValueObject<T> which does not match the T in public abstract bool Equals(T obj).

In fact, at runtime it could be of a different class than T.

Probably, you want to return false in that case.

public static bool operator ==(ValueObject<T> obj1, ValueObject<T> obj2)
{
    if (object.ReferenceEquals(obj1, obj2)) return true;
    if (object.ReferenceEquals(obj1, null)) return false;
    if (object.ReferenceEquals(obj2, null)) return false;
    if (obj1.GetType() != obj2.GetType()) return false; //new

    return ((T)(object)obj1).Equals((T)(object)obj2);
}

I think you indend that T is equal to a derived class of ValueObject<T>. You can say where T : ValueObject<T>. That encodes at least a part of what you want in the type system and saves you the (object) cast.

usr
  • 168,620
  • 35
  • 240
  • 369
  • It is allowed but it doesn't do what you want it to. When you say `class V where T : V {} class C : V {}` then `C` matches the pattern, but what stops you from then saying `class D : V` ? Now it doesn't match the pattern. The pattern you want actually cannot be represented in the C# type system; try Haskell if you want a type system that can represent that. – Eric Lippert Mar 15 '14 at 14:29
  • @EricLippert yeah I think it does save the `(object)` cast, though. Fewer contortions required. – usr Mar 15 '14 at 14:32
  • So the idea is to add a confusing constraint to the public surface of the object in order to save typing eight characters as the implementation details of a method? – Eric Lippert Mar 15 '14 at 14:37
  • @EricLippert mostly to exclude a few invalid uses/mistakes. This pattern is dirty, anyway. We agree that it is impossible to implement it in a clean way. Btw, it's 16 characters! :) – usr Mar 15 '14 at 14:39