20

I am aware of the fact that I always have to override Equals(object) and GetHashCode() when implementing IEquatable<T>.Equals(T).

However, I don't understand, why in some situations the Equals(object) wins over the generic Equals(T).

For example why is the following happening? If I declare IEquatable<T> for an interface and implement a concrete type X for it, the general Equals(object) is called by a Hashset<X> when comparing items of those type against each other. In all other situations where at least one of the sides is cast to the Interface, the correct Equals(T) is called.

Here's a code sample to demonstrate:

public interface IPerson : IEquatable<IPerson> { }

//Simple example implementation of Equals (returns always true)
class Person : IPerson
{
    public bool Equals(IPerson other)
    {
        return true;
    }

    public override bool Equals(object obj)
    {
        return true;
    }

    public override int GetHashCode()
    {
        return 0;
    }
}

private static void doEqualityCompares()
{
    var t1 = new Person();

    var hst = new HashSet<Person>();
    var hsi = new HashSet<IPerson>();

    hst.Add(t1);
    hsi.Add(t1);

    //Direct comparison
    t1.Equals(t1);                  //IEquatable<T>.Equals(T)

    hst.Contains(t1);               //Equals(object) --> why? both sides inherit of IPerson...
    hst.Contains((IPerson)t1);      //IEquatable<T>.Equals(T)

    hsi.Contains(t1);               //IEquatable<T>.Equals(T)
    hsi.Contains((IPerson)t1);      //IEquatable<T>.Equals(T)
}
meJustAndrew
  • 6,011
  • 8
  • 50
  • 76
Marwie
  • 3,177
  • 3
  • 28
  • 49
  • 1
    shouldn't `hst.Contains((IPerson)t1);` fail to compile? – Dennis_E Feb 10 '15 at 15:21
  • @Dennis_E See the second half of my answer. – Servy Feb 10 '15 at 15:23
  • 1
    @Dennis_E: that's not `HashSet.Contains` but `Enumerable.Contains` which accepts the interface. So this is the slow version that enumerates all elements and compares them with `Equals(IPerson other)` instead of using `GetHashCode`. – Tim Schmelter Feb 10 '15 at 15:25
  • 1
    Ok, I did not think about that. I tried it on ideone.com and it failed to compile. But that's because I didn't add `using System.Linq;` – Dennis_E Feb 10 '15 at 15:28
  • Every time someone complains Java has more confusing behaviour than C# I should point them to this question. – user253751 Feb 10 '15 at 21:17

2 Answers2

21

HashSet<T> calls EqualityComparer<T>.Default to get the default equality comparer when no comparer is provided.

EqualityComparer<T>.Default determines if T implementsIEquatable<T>. If it does, it uses that, if not, it uses object.Equals and object.GetHashCode.

Your Person object implements IEquatable<IPerson> not IEquatable<Person>.

When you have a HashSet<Person> it ends up checking if Person is an IEquatable<Person>, which its not, so it uses the object methods.

When you have a HashSet<IPerson> it checks if IPerson is an IEquatable<IPerson>, which it is, so it uses those methods.


As for the remaining case, why does the line:

hst.Contains((IPerson)t1);

call the IEquatable Equals method even though its called on the HashSet<Person>. Here you're calling Contains on a HashSet<Person> and passing in an IPerson. HashSet<Person>.Contains requires the parameter to be a Person; an IPerson is not a valid argument. However, a HashSet<Person> is also an IEnumerable<Person>, and since IEnumerable<T> is covariant, that means it can be treated as an IEnumerable<IPerson>, which has a Contains extension method (through LINQ) which accepts an IPerson as a parameter.

IEnumerable.Contains also uses EqualityComparer<T>.Default to get its equality comparer when none is provided. In the case of this method call we're actually calling Contains on an IEnumerable<IPerson>, which means EqualityComparer<IPerson>.Default is checking to see if IPerson is an IEquatable<IPerson>, which it is, so that Equals method is called.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • I see - so the `EqualityComparer.Default` can actually only get the `Equals(T)` method if it is exactly of the type T - it wouldn't be able to get it of a type T' which inherited of T. I thought there was polymorphism in the game but obviously I was completely wrong. Thanks for the clarification. – Marwie Feb 10 '15 at 15:49
  • It should be noted that they have chosen *not* to make `IEquatable` contravariant in its type argument `T`. If they had made `IEquatable<>` contravariant (would have been `IEquatable`), something that was an `IEquatable` would implicitly have been an `IEquatable` as well. However, I guess making it contravariant would have led to even worse problems (than what we already have) when people derive from classes that implement `IEquatable<>`. ***Edit:*** Ah, I see supercat already mentioned this in the other answer. – Jeppe Stig Nielsen Apr 29 '15 at 09:06
2

Although IComparable<in T> is contravariant with respect to T, such that any type which implements IComparable<Person> would automatically be considered an implementation of IComparable<IPerson>, the type IEquatable<T> is intended for use with sealed types, especially structures. The requirement that Object.GetHashCode() be consistent with both IEquatable<T>.Equals(T) and Object.Equals(Object) generally implies that the latter two methods should behave identically, which in turn implies that one of them should chain to the other. While there is a large performance difference between passing a struct directly to an IEquatable<T> implementation of the proper type, compared with constructing a instance of the structure's boxed-heap-object type and having an Equals(Object) implementation copy the structure data out of that, no such performance different exists with reference types. If IEquatable<T>.Equals(T) and Equals(Object) are going to be equivalent and T is an inheritable reference type, there's no meaningful difference between:

bool Equals(MyType obj)
{
  MyType other = obj as MyType;
  if (other==null || other.GetType() != typeof(this))
    return false;
  ... test whether other matches this
}

bool Equals(MyType other)
{
  if (other==null || other.GetType() != typeof(this))
    return false;
  ... test whether other matches this
}

The latter could save one typecast, but that's unlikely to make a sufficient performance difference to justify having two methods.

supercat
  • 77,689
  • 9
  • 166
  • 211