21

I have two lists that I am trying to compare. So I have created a class that implements the IEqualityComparer interface, please see below in the bottom section of code.

When I step through my code, the code goes through my GetHashCode implementation but not the Equals? I do not really understand the GetHashCode method, despite reading around on the internet and what exactly it is doing.

List<FactorPayoffs> missingfactorPayoffList = 
    factorPayoffList.Except(
        factorPayoffListOrg,
        new FactorPayoffs.Comparer()).ToList();

List<FactorPayoffs> missingfactorPayoffListOrg =
    factorPayoffListOrg.Except(
        factorPayoffList,
        new FactorPayoffs.Comparer()).ToList();

So in the two lines of code above the two lists return me every item, telling me that the two lists do not contain any items that are the same. This is not true, there is only row that is different. I'm guessing this is happening because the Equals method is not getting called which in turns makes me wonder if my GetHashCode method is working as its supposed to?

class FactorPayoffs
    {
        public string FactorGroup { get; set; }
        public string Factor { get; set; }
        public DateTime dtPrice { get; set; }
        public DateTime dtPrice_e { get; set; }
        public double Ret_USD { get; set; }

        public class Comparer : IEqualityComparer<FactorPayoffs>
        {
            public bool Equals(FactorPayoffs x, FactorPayoffs y)
            {                    
                return x.dtPrice == y.dtPrice && 
                    x.dtPrice_e == y.dtPrice_e && 
                    x.Factor == y.Factor && 
                    x.FactorGroup == y.FactorGroup;
            }

            public int GetHashCode(FactorPayoffs obj)
            {
                int hash = 17;
                hash = hash * 23 + (obj.dtPrice).GetHashCode();
                hash = hash * 23 + (obj.dtPrice_e).GetHashCode();
                hash = hash * 23 + (obj.Factor ?? "").GetHashCode();
                hash = hash * 23 + (obj.FactorGroup ?? "").GetHashCode();
                hash = hash * 23 + (obj.Ret_USD).GetHashCode();
                return hash;
            }
        }
    }
Jodrell
  • 34,946
  • 5
  • 87
  • 124
mHelpMe
  • 6,336
  • 24
  • 75
  • 150
  • The `GetHashCode` implementation has to return the same value when two instances are considered equal. It should try to return different numbers when they are not equal but this is of lower importance. – Jodrell Mar 17 '14 at 11:32

4 Answers4

35

Your Equals and GetHashCode implementations should involve the exact same set of properties; they do not.

In more formal terms, GetHashCode must always return the same value for two objects that compare equal. With your current code, two objects that differ only in the Ret_USD value will always compare equal but are not guaranteed to have the same hash code.

So what happens is that LINQ calls GetHashCode on two objects you consider equal, gets back different values, concludes that since the values were different the objects cannot be equal so there's no point at all in calling Equals and moves on.

To fix the problem, either remove the Ret_USD factor from GetHashCode or introduce it also inside Equals (whatever makes sense for your semantics of equality).

Jon
  • 428,835
  • 81
  • 738
  • 806
  • thanks - it now works. Think I need to read more on hashcodes, I'm not understanding how they are being generated – mHelpMe Mar 17 '14 at 11:37
  • 3
    @mHelpMe [Here's a valuable link](http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) – Sriram Sakthivel Mar 17 '14 at 11:38
  • @mHelpMe, aslo, arithmetic should be unchecked and avoid pointless null coalescence, http://stackoverflow.com/a/22453411/659190 – Jodrell Mar 17 '14 at 11:48
  • Link in previous comment didn't work for me. I'm guessing it was this one https://ericlippert.com/2011/02/28/guidelines-and-rules-for-gethashcode/ – joym8 Sep 16 '21 at 20:05
  • @SriramSakthivel: Your link is broken. Updated link is here: [Guidelines and rules for GetHashCode](https://ericlippert.com/2011/02/28/guidelines-and-rules-for-gethashcode/). It is really a must read for all developers. – Mustafa Özçetin Aug 07 '23 at 12:21
15

GetHashCode is intended as a fast but rough estimate of equality, so many operations potentially involving large numbers of comparisons start by checking this result instead of Equals, and only use Equals when necessary. In particular, if x.GetHashCode()!=y.GetHashCode(), then we already know x.Equals(y) is false, so there is no reason to call Equals. Had x.GetHashCode()==y.GetHashCode(), then x might equal y, but only a call to Equals will give a definite answer.

If you implement GetHashCode in a way that causes GetHashCode to be different for two objects where Equals returns true, then you have a bug in your code and many collection classes and algorithms relying on these methods will silently fail.

Sam Harwell
  • 97,721
  • 20
  • 209
  • 280
10

If you want to force the execution of the Equals you can implement it as follows

public int GetHashCode(FactorPayoffs obj) {
        return 1;
    }
  • This is an easy workaround but be careful when using this approach. When used within a tight loop, this can cause serious performance penalties since you are bypassing the `GetHashCode()` method for all objects. – Mustafa Özçetin Aug 07 '23 at 15:04
3

Rewrite you GetHashCode implementation like this, to match the semantics of your Equals implementation.

public int GetHashCode(FactorPayoffs obj)
{
    unchecked
    {
            int hash = 17;
            hash = hash * 23 + obj.dtPrice.GetHashCode();
            hash = hash * 23 + obj.dtPrice_e.GetHashCode();
            if (obj.Factor != null)
            {
                hash = hash * 23 + obj.Factor.GetHashCode();
            }

            if (obj.FactorGroup != null)
            {
                hash = hash * 23 + obj.FactorGroup.GetHashCode();
            }

            return hash;
    }
}

Note, you should use unchecked because you don't care about overflows. Additionaly, coalescing to string.Empty is pointlessy wasteful, just exclude from the hash.

See here for the best generic answer I know,

Community
  • 1
  • 1
Jodrell
  • 34,946
  • 5
  • 87
  • 124