Search code examples
c#dictionaryhashcodeiequatable

Class implementation of IEquatable for use as a key in a dictionary


I've got a class which consists of two strings and an enum. I'm trying to use instances of this class as keys in a dictionary. Unfortunately I don't seem to be implementing IEquatable properly. Here's how I've done it:

public enum CoinSide
{
    Heads,
    Tails
}

public class CoinDetails : IComparable, IEquatable<CoinDetails>
{
    private string denomination;
    private string design;
    private CoinSide side;

//...

    public int GetHashCode(CoinDetails obj)
    {
        return string.Concat(obj.Denomination, obj.Design, obj.Side.ToString()).GetHashCode();
    }

    public bool Equals(CoinDetails other)
    {
        return (this.Denomination == other.Denomination && this.Design == other.Design && this.Side == other.Side);
    }
}

However, I still can't seem to look up items in my dictionary. Additionally, the following tests fail:

    [TestMethod]
    public void CoinDetailsHashCode()
    {
        CoinDetails a = new CoinDetails("1POUND", "1997", CoinSide.Heads);
        CoinDetails b = new CoinDetails("1POUND", "1997", CoinSide.Heads);
        Assert.AreEqual(a.GetHashCode(), b.GetHashCode());
    }

    [TestMethod]
    public void CoinDetailsCompareForEquality()
    {
        CoinDetails a = new CoinDetails("1POUND", "1997", CoinSide.Heads);
        CoinDetails b = new CoinDetails("1POUND", "1997", CoinSide.Heads);
        Assert.AreEqual<CoinDetails>(a, b);
    }

Would someone be able to point out where I'm going wrong? I'm sure I'm missing something rather simple, but I'm not sure what.


Solution

  • You class has to override Equals and GetHashCode:

    public class CoinDetails 
    {
        private string Denomination;
        private string Design;
        private CoinSide Side;
    
        public override bool Equals(object obj)
        {
            CoinDetails c2 = obj as CoinDetails;
            if (c2 == null)
                return false;
            return Denomination == c2.Denomination && Design == c2.Design;
        }
    
        public override int GetHashCode()
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + (Denomination ?? "").GetHashCode();
                hash = hash * 23 + (Design ?? "").GetHashCode();
                return hash;
            }
        }
    }
    

    Note that i've also improved your GetHashCode algorithm according to: What is the best algorithm for an overridden System.Object.GetHashCode?

    You could also pass a custom IEqualityComparer<CoinDetail> to the dictionary:

    public class CoinComparer : IEqualityComparer<CoinDetails>
    {
        public bool Equals(CoinDetails x, CoinDetails y)
        {
            if (x == null || y == null) return false;
            if(object.ReferenceEquals(x, y)) return true;
            return x.Denomination == y.Denomination && x.Design == y.Design;
        }
    
        public int GetHashCode(CoinDetails obj)
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + (obj.Denomination ?? "").GetHashCode();
                hash = hash * 23 + (obj.Design ?? "").GetHashCode();
                return hash;
            }
        }                      
    }
    

    Now this works and does not require CoinDetails to override Equals+GetHashCode:

    var dict = new Dictionary<CoinDetails, string>(new CoinComparer());
    dict.Add(new CoinDetails("1POUND", "1997"), "");
    dict.Add(new CoinDetails("1POUND", "1997"), ""); // FAIL!!!!