views:

738

answers:

4

Many of my questions here on SO concerns IEquatable implementation. I found it being extremely difficult to implement correctly, because there are many hidden bugs in the naïve implementation, and the articles I found about it are quite incomplete. I want to find or write a definitive reference which must include:

  • How to implement IEquatable correctly
  • How to override Equals correctly
  • How to override GetHashCode correctly
  • How to implement the ToString method correctly
  • How to implement the operator == correctly
  • How to implement the operator != correctly

Such a complete reference already exists?

PS: Even MSDN reference seems flawed to me

+2  A: 

Upon reading MSDN, I'm pretty certain the best example of a proper implementation is in the IEquatable.Equals Method page. My only deviation is the following:

public override bool Equals(Object obj)
{
   if (obj == null) return base.Equals(obj);

   if (! (obj is Person))
      return false; // Instead of throw new InvalidOperationException
   else
      return Equals(obj as Person);   
}

For those wondering about the deviation, it derives from the Object.Equals(Object) MSDN page:

Implementations of Equals must not throw exceptions.

sixlettervariables
Also implementors should be wary of overloading '==' and then accidentally using it in Equals(T object). I recommend using Object.ReferenceEquals to handle checking reference equality in any of the Equals-style methods.
sixlettervariables
+1 i was just going to mention ReferenceEquals
Stan R.
Should we edit your post to replace == by ReferenceEquals?
Jader Dias
obj == null, shouldn't invoke the operator== since it is still an object at that point and not a more derived class.
sixlettervariables
+1  A: 

Implementing IEquatable<T> for a Value Type

Implementing IEquatable<T> for a value type is a little bit different than for a reference type. Let's assume we have the Implement-Your-Own-Value-Type archetype, a Complex number struct.

public struct Complex
{
    public double RealPart { get; set; }
    public double ImaginaryPart { get; set; }
}

Our first step would be to implement IEquatable<T> and override Object.Equals and Object.GetHashCode:

public bool Equals(Complex other)
{
    // Complex is a value type, thus we don't have to check for null
    // if (other == null) return false;

    return (this.RealPart == other.RealPart)
        && (this.ImaginaryPart == other.ImaginaryPart);
}

public override bool Equals(object other)
{
    // other could be a reference type, thus we check for null
    if (other == null) return base.Equals(other);

    if (other is Complex)
    {
        return this.Equals((Complex)other);
    }
    else
    {
        return false;
    }
}

public override int GetHashCode()
{
    return this.RealPart.GetHashCode() ^ this.ImaginaryPart.GetHashCode();
}

With very little effort we have a correct implementation, excepting the operators. Adding the operators is also a trivial process:

public static bool operator ==(Complex term1, Complex term2)
{
    return term1.Equals(term2);
}

public static bool operator !=(Complex term1, Complex term2)
{
    return !term1.Equals(term2);
}

An astute reader would notice that we should probably implement IEquatable<double> since Complex numbers could be interchangeable with the underlying value type.

public bool Equals(double otherReal)
{
    return (this.RealPart == otherReal) && (this.ImaginaryPart == 0.0);
}

public override bool Equals(object other)
{
    // other could be a reference type, thus we check for null
    if (other == null) return base.Equals(other);

    if (other is Complex)
    {
        return this.Equals((Complex)other);
    }
    else if (other is double)
    {
        return this.Equals((double)other);
    }
    else
    {
        return false;
    }
}

We need four operators if we add IEquatable<double>, because you can have Complex == double or double == Complex (and the same for operator !=):

public static bool operator ==(Complex term1, double term2)
{
    return term1.Equals(term2);
}

public static bool operator ==(double term1, Complex term2)
{
    return term2.Equals(term1);
}

public static bool operator !=(Complex term1, double term2)
{
    return !term1.Equals(term2);
}

public static bool operator !=(double term1, Complex term2)
{
    return !term2.Equals(term1);
}

So there you have it, with minimal effort we have a correct and useful implementation IEquatable<T> for a value type:

public struct Complex : IEquatable<Complex>, IEquatable<double>
{
}
sixlettervariables
Your Equals(Complex other) implementation throws exception when other is null
Jader Dias
your operators implementations also throw exceptions when the first parameter is null
Jader Dias
@Jader: thanks, I've fixed the first problem. Trying to conceive of a situation where the 2nd could actually happen given error CS0037: Cannot convert null to Complex because it is a non-nullable value type.
sixlettervariables
Additionally, "null == c1" and "c1 == null" do not use any of Complex's code, but rather object.operator==.
sixlettervariables
+1  A: 

I finally wrote a sample code that shows the correct way to implement all IEquatable related methods:

http://code.google.com/p/iequatable-implementation-reference/source/browse/IEquatableReference/trunk/IEquatableReference/ValuesClass.cs

The unit tests that justify each line of code are there:

http://code.google.com/p/iequatable-implementation-reference/source/browse/IEquatableReference/trunk/IEquatableReferenceTest/ValuesClassTest.cs

I just didn't write a unit test for GetHashCode arithmetic overflow.

Jader Dias
I'm not keen on using XOR for the hashes. It makes the order irrelevant so hash(x, y) == hash (y, x) and it gives 0 for all hash(x, x). In a lot of cases (x, x) is more likely to occur than other pairings. I usually use Josh Bloch's suggested "start with a prime, then repeatedly multiply by 31 and add the next hash."
Jon Skeet
(Use an unchecked context to avoid arithmetic overflows; they don't matter in terms of the hash.)
Jon Skeet
You might also want to consider whether == and Equals should really return the same thing when one of your components is a double, because double itself *doesn't* obey that (for double.NaN).
Jon Skeet
Finally, any reason for implementing != explicitly instead of returning !(valuesClassA == valuesClassB)?
Jon Skeet
You are right about your last comment, thanks! I am correcting this...
Jader Dias
@Jon Skeet I just commited suggestions 1, 2 and 4
Jader Dias
Cool. My third suggestion was really a note of interest more than anything else. Doubles behave weirdly - pretty much whatever you do will surprise someone.
Jon Skeet
@Jon, if you're using something whose identity revolves around something like a float or double, I think it is important to keep the same equality semantics. So NaN != NaN should be stay true even in your derived type. Now, that isn't to say you have some KeyValuePair style semantics where you need something more like IsNan(a) == IsNan(b).
sixlettervariables
A: 

I found another reference, it's the .NET Anonymous Type implementation. For an anonymous type with an int and a double as properties I disassembled the following C# code:

public class f__AnonymousType0
{
    // Fields
    public int A { get; }
    public double B { get; }

    // Methods
    public override bool Equals(object value)
    {
        var type = value as f__AnonymousType0;
        return (((type != null)
            && EqualityComparer<int>.Default.Equals(this.A, type.A))
            && EqualityComparer<double>.Default.Equals(this.B, type.B));
    }

    public override int GetHashCode()
    {
        int num = -1134271262;
        num = (-1521134295 * num) + EqualityComparer<int>.Default.GetHashCode(this.A);
        return ((-1521134295 * num) + EqualityComparer<double>.Default.GetHashCode(this.B);
    }

    public override string ToString()
    {
        StringBuilder builder = new StringBuilder();
        builder.Append("{ A = ");
        builder.Append(this.A);
        builder.Append(", B = ");
        builder.Append(this.B);
        builder.Append(" }");
        return builder.ToString();
    }
}
Jader Dias