Is there a complete IEquatable implementation reference?
Asked Answered
B

5

57

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

Bougie answered 20/8, 2009 at 16:48 Comment(0)
J
33

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, the is operator will return false if null
    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>
{
}
Jadotville answered 20/8, 2009 at 17:48 Comment(10)
Your Equals(Complex other) implementation throws exception when other is nullBougie
your operators implementations also throw exceptions when the first parameter is nullBougie
@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.Jadotville
Additionally, "null == c1" and "c1 == null" do not use any of Complex's code, but rather object.operator==.Jadotville
"other could be a reference type, thus we check for null" - Seems like this test is redundant, that case is covered by the 'other is Complex' test that follows it.Septilateral
In your first example at the weak-type equals, you could just check for is parameter, which will return false if other is either null or a different type. I've updated your post, feel free to rollback or comment if you disagree.Inhaul
An implementation of Equals(Object) which would cause a Complex to report itself equal to a double is illegitimate, because such a comparison would not be reflexive; I would suggest that if double widens to Complex, an Equals(double) overload should exist but have an [Obsolete] tag, to prevent a Complex/double comparison from yielding results inconsistent with a double/Complex comparison. To be sure, there are enough bad spots in the framework (e.g. (1.0).Equals(1.0f) converts 1.0f to double and yields true; (1.0f).Equals(1.0) converts 1.0 to object and yields false)Pippas
@supercat: there is an edit link you can use to correct problems in posts (not everybody stays around to use the site anymore).Jadotville
@user7116: It is generally bad form to edit a post to deliberately say something materially different from what an author meant to say. The post advised implementing Equals so as to violate the expectation of transitivity; I disagree with that advice, and would hope that the author would reconsider, but am loath to alter advise that was deliberately given.Pippas
@supercat: are you new to SO? Editing is pretty fundamental to the site's operation. Not sure what advice was given insofar as an example of a value type...nobody is designing complex number systems off this answer. I'll delete the answer so as to make it obvious I guess.Jadotville
I
16

I believe getting something as simple as checking objects for equality correct is a bit tricky with .NET's design.

For Struct

1) Implement IEquatable<T>. It improves performance noticeably.

2) Since you're having your own Equals now, override GetHashCode, and to be consistent with various equality checking override object.Equals as well.

3) Overloading == and != operators need not be religiously done since the compiler will warn if you unintentionally equate a struct with another with a == or !=, but its good to do so to be consistent with Equals methods.

public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

For Class

From MS:

Most reference types should not overload the equality operator, even if they override Equals.

To me == feels like value equality, more like a syntactic sugar for Equals method. Writing a == b is much more intuitive than writing a.Equals(b). Rarely we'll need to check reference equality. In abstract levels dealing with logical representations of physical objects this is not something we would need to check. I think having different semantics for == and Equals can actually be confusing. I believe it should have been == for value equality and Equals for reference (or a better name like IsSameAs) equality in the first place. I would love to not take MS guideline seriously here, not just because it isn't natural to me, but also because overloading == doesn't do any major harm. That's unlike not overriding non-generic Equals or GetHashCode which can bite back, because framework doesn't use == anywhere but only if we ourself use it. The only real benefit I gain from not overloading == and != will be the consistency with design of the entire framework over which I have no control of. And that's indeed a big thing, so sadly I will stick to it.

With reference semantics (mutable objects)

1) Override Equals and GetHashCode.

2) Implementing IEquatable<T> isn't a must, but will be nice if you have one.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

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

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

With value semantics (immutable objects)

This is the tricky part. Can get easily messed up if not taken care..

1) Override Equals and GetHashCode.

2) Overload == and != to match Equals. Make sure it works for nulls.

2) Implementing IEquatable<T> isn't a must, but will be nice if you have one.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

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

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Take special care to see how it should fare if your class can be inherited, in such cases you will have to determine if a base class object can be equal to a derived class object. Ideally, if no objects of derived class is used for equality checking, then a base class instance can be equal to a derived class instance and in such cases, there is no need to check Type equality in generic Equals of base class.

In general take care not to duplicate code. I could have made a generic abstract base class (IEqualizable<T> or so) as a template to allow re-use easier, but sadly in C# that stops me from deriving from additional classes.

Iraq answered 16/12, 2012 at 22:31 Comment(1)
I know it has been a few years by now, but looking into: learn.microsoft.com/en-us/dotnet/api/… ""In addition, you should overload the op_Equality and op_Inequality operators. This ensures that all tests for equality return consistent results.""" also mutable objects should not implement IEquatable since it would mean, the hashCode could change over life time.Tryma
J
3

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.

Jadotville answered 20/8, 2009 at 17:7 Comment(2)
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.Jadotville
obj == null, shouldn't invoke the operator== since it is still an object at that point and not a more derived class.Jadotville
B
3

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();
    }
}
Bougie answered 16/9, 2009 at 1:19 Comment(0)
B
1

I only have to derive from this class

public abstract class DataClass : IEquatable<DataClass>
{
    public override bool Equals(object obj)
    {
        var other = obj as DataClass;
        return this.Equals(other);
    }

    public bool Equals(DataClass other)
    {
        return (!ReferenceEquals(null, other))
            && this.Execute((self2, other2) =>
                other2.Execute((other3, self3) => self3.Equals(other3), self2)
                , other);
    }

    public override int GetHashCode()
    {
        return this.Execute(obj => obj.GetHashCode());
    }

    public override string ToString()
    {
        return this.Execute(obj => obj.ToString());
    }

    private TOutput Execute<TOutput>(Func<object, TOutput> function)
    {
        return this.Execute((obj, other) => function(obj), new object());
    }

    protected abstract TOutput Execute<TParameter, TOutput>(
        Func<object, TParameter, TOutput> function,
        TParameter other);
}

And then implement the abstract method like this

public class Complex : DataClass
{
    public double Real { get; set; }

    public double Imaginary { get; set; }

    protected override TOutput Execute<TParameter, TOutput>(
        Func<object, TParameter, TOutput> function,
        TParameter other)
    {
        return function(new
        {
            Real = this.Real,
            Imaginary = this.Imaginary,
        }, other);
    }
}
Bougie answered 22/12, 2011 at 14:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.