Search code examples
c#.netinterfacegethashcodeiequatable

Overriding IEquatable<T> when T is an interface and hashcodes are different between derived types


I have A and B classes both implementing interface I.

public interface I
{
    int SomeInt { get; }
    bool SomeBool { get; }
    float SomeFloat { get; }
}


public class A : I
{
    public int SomeInt { get; }
    public bool SomeBool { get; }
    public float SomeFloat { get; }

    private readonly string _someARelatedStuff;
    // Rest of class...
}

public class B : I
{
    public int SomeInt { get; }
    public bool SomeBool { get; }
    public float SomeFloat { get; }

    private string readonly _someBRelatedStuff;
    private double readonly _someOtherBRelatedStuff;
    // Rest of class...
}

Sometimes I want to test equality between A and B (usually when comparing lists of A and lists of B) based on the equality of their I properties (SomeInt, SomeBool, SomeFloat), so I implemented IEquatable<I> on both and I compare them based on their shared I properties values.

The problem is that I already have an implementation for GetHashCode() on both A and B that produces different hashes because I'm taking into account additional members.

B does not depend on A so I use interface I to compare them and it has a list of properties with getters.

I read in a StackOverflow answer that:

If you are implementing a class, you should always make sure that two equal objects have the same hashcode.

So does that mean that everytime a class A want to be implement interface I, and I want to be able to compare instances that implement I, I have to make sure the hashcode is calculated in the same way for all instances of I and only use I properties?

I do feel like I'm not intended to implement IEquatable<T> when T is an interface, but my alternatives are:

  1. Using regular inheritance with a base class - I rather avoid inheritance when possible, and this solution won't work if B needs to derive from some framework C class because of single inheritance
  2. Implement equality checks between A and B with a method on either A or B - will create code duplication
  3. Have an equality check method between I instances defined in I - sounds like the best option

Are there any options that I'm missing?


Solution

  • Consider making the a IEqualityComparer<> class to compare the common values.

    I have renamed the interface to ICommon for readability

    public interface ICommon
    {
        int SomeInt { get; }
        bool SomeBool { get; }
        float SomeFloat { get; }
    }
    
    public class CommonComparer : IEqualityComparer<ICommon>
    {
        public bool Equals(ICommon x, ICommon y)
        {
            return x.SomeInt.Equals(y.SomeInt)
                && x.SomeBool.Equals(y.SomeBool)
                && x.SomeFloat.Equals(y.SomeFloat);
        }
    
        public int GetHashCode(ICommon obj)
        {
            unchecked
            {
                int hc = -1817952719;
                hc = (-1521134295)*hc + obj.SomeInt.GetHashCode();
                hc = (-1521134295)*hc + obj.SomeBool.GetHashCode();
                hc = (-1521134295)*hc + obj.SomeFloat.GetHashCode();
                return hc;
            }
        }
    }
    

    and the program can distinguish between the equal items on two lists.

    class Program
    {
        static void Main(string[] args)
        {
            var listA = new List<A>
            {
                new A(1001, true, 1.001f, "A1"),
                new A(1002, true, 1.002f, "A2"),
                new A(1003, false, 1.003f, "A1"),
                new A(1004, false, 1.004f, "A4")
            };
    
            var listB = new List<B>
            {
                new B(1001, true, 1.001f, "B1", 2.5),
                new B(1002, false, 1.002f, "B2", 2.8),
                new B(1003, true, 1.003f, "B3", 2.9),
                new B(1004, false, 1.004f, "B4", 2.9)
            };
    
            var common = Enumerable.Intersect(listA, listB, new CommonComparer()).OfType<ICommon>();
    
    
            Console.WriteLine($"{"SomeInt",-8} {"Bool",-6} {"SomeFloat",-10}");
            foreach (var item in common)
            {
                Console.WriteLine($"{item.SomeInt,-8} {item.SomeBool,-6} {item.SomeFloat,-10}");
            }
            //SomeInt  Bool   SomeFloat
            //1001     True   1.001
            //1004     False  1.004
    
        }
    }
    

    and the rest of the code definitions

    public class A : ICommon, IEquatable<A>
    {
        static readonly CommonComparer comparer = new CommonComparer();
    
        public int SomeInt { get; }
        public bool SomeBool { get; }
        public float SomeFloat { get; }
    
        private readonly string _someARelatedStuff;
        // Rest of class...
        public A(ICommon other, string someARelatedStuff)
            : this(other.SomeInt, other.SomeBool, other.SomeFloat, someARelatedStuff)
        { }
        public A(int someInt, bool someBool, float someFloat, string someARelatedStuff)
        {
            this.SomeInt = someInt;
            this.SomeBool = someBool;
            this.SomeFloat = someFloat;
            this._someARelatedStuff = someARelatedStuff;
        }
    
        public override string ToString() => _someARelatedStuff;
    
        #region IEquatable Members
        public override bool Equals(object obj)
        {
            if (obj is A other)
            {
                return Equals(other);
            }
            return false;
        }
    
    
        public virtual bool Equals(A other)
        {
            return comparer.Equals(this, other)
                && _someARelatedStuff.Equals(other._someARelatedStuff);
        }
    
        public override int GetHashCode()
        {
            unchecked
            {
                int hc = comparer.GetHashCode(this);
                hc = (-1521134295)*hc + _someARelatedStuff.GetHashCode();
                return hc;
            }
        }
    
        #endregion
    
    }
    
    public class B : ICommon, IEquatable<B>
    {
        static readonly CommonComparer comparer = new CommonComparer();
    
        public int SomeInt { get; }
        public bool SomeBool { get; }
        public float SomeFloat { get; }
    
        readonly string _someBRelatedStuff;
        readonly double _someOtherBRelatedStuff;
        // Rest of class...
    
        public B(ICommon other, string someBRelatedStuff, double someOtherBRelatedStuff)
            : this(other.SomeInt, other.SomeBool, other.SomeFloat, someBRelatedStuff, someOtherBRelatedStuff)
        { }
        public B(int someInt, bool someBool, float someFloat, string someBRelatedStuff, double someOtherBRelatedStuff)
        {
            this.SomeInt = someInt;
            this.SomeBool = someBool;
            this.SomeFloat = someFloat;
            this._someBRelatedStuff = someBRelatedStuff;
            this._someOtherBRelatedStuff = someOtherBRelatedStuff;
        }
    
        public override string ToString() => $"{_someBRelatedStuff}, {_someOtherBRelatedStuff.ToString("g4")}";
    
        #region IEquatable Members
    
        public override bool Equals(object obj)
        {
            if (obj is B other)
            {
                return Equals(other);
            }
            return false;
        }
    
        public virtual bool Equals(B other)
        {
            return comparer.Equals(this, other)
                && _someBRelatedStuff.Equals(other._someBRelatedStuff)
                && _someOtherBRelatedStuff.Equals(other._someOtherBRelatedStuff);
        }
    
        public override int GetHashCode()
        {
            unchecked
            {
                int hc = comparer.GetHashCode(this);
                hc = (-1521134295)*hc + _someBRelatedStuff.GetHashCode();
                hc = (-1521134295)*hc + _someOtherBRelatedStuff.GetHashCode();
                return hc;
            }
        }
    
        #endregion
    }