21
public class Address{
    public string ContactName {get; private set;}
    public string Company {get; private set;}
    //...
    public string Zip {get; private set;}
}

I'd like to implement a notion of distint addresses, so I overrode Equals() to test for case-insensitive equality in all of the fields (as these are US addresses, I used Ordinal instead of InvariantCulture for maximum performance):

public override bool Equals(Object obj){
    if (obj == null || this.GetType() != obj.GetType())
        return false;

    Address o = (Address)obj;

    return  
    (string.Compare(this.ContactName, o.ContactName, StringComparison.OrdinalIgnoreCase) == 0) &&
    (string.Compare(this.Company, o.Company, StringComparison.OrdinalIgnoreCase) == 0)
    // ...
    (string.Compare(this.Zip, o.Zip, StringComparison.OrdinalIgnoreCase) == 0)
}

I'd like to write a GetHashCode() similarly like so (ignore the concatenation inefficiency for the moment):

public override int GetHashCode(){
    return (this.contactName + this.address1 + this.zip).ToLowerOrdinal().GetHashCode();
}

but that doesn't exist. What should I use instead? Or should I just use InvariantCulture in my Equals() method?

(I'm thinking .ToLowerInvariant().GetHashCode(), but I'm not 100% sure that InvariantCulture can't decide that an identical character (such as an accent) has a different meaning in another context.)

Community
  • 1
  • 1
Arithmomaniac
  • 4,604
  • 3
  • 38
  • 58

3 Answers3

36

Whatever string comparison method you use in Equals(), it makes sense to use the same in GetHashCode().

There's no need to create temporary strings just to calculate hash codes. For StringComparison.OrdinalIgnoreCase, use StringComparer.OrdinalIgnoreCase.GetHashCode()

Then you need to combine multiple hash codes into one. XOR should be ok (because it's unlikely that one person's zip code is another's contact name). However purists might disagree.

public override int GetHashCode()
{
    return StringComparer.OrdinalIgnoreCase.GetHashCode(ContactName) ^
        StringComparer.OrdinalIgnoreCase.GetHashCode(Company) ^
        // ...
        StringComparer.OrdinalIgnoreCase.GetHashCode(Zip);
}

Having said all that, I'd question whether it's sensible to use a composite structure like Address as the key to a dictionary. But the principle holds for identity-type strings.

Community
  • 1
  • 1
ben
  • 1,441
  • 2
  • 16
  • 21
17

Two unequal objects can have the same hashcode. Though two equal objects should never have different hashcodes. If you use InvariantCulture for your hashcode it will still be correct as far as the contract for Equals goes if it's implemented in terms of OrdinalIgnoreCase.

From the documentation on StringComparer.OrdinalIgnoreCase (emphasis mine):

http://msdn.microsoft.com/en-us/library/system.stringcomparer.ordinalignorecase.aspx

The StringComparer returned by the OrdinalIgnoreCase property treats the characters in the strings to compare as if they were converted to uppercase using the conventions of the invariant culture, and then performs a simple byte comparison that is independent of language. This is most appropriate when comparing strings that are generated programmatically or when comparing case-insensitive resources such as paths and filenames.

Steve Dunn
  • 21,044
  • 11
  • 62
  • 87
Joe Castro
  • 2,181
  • 18
  • 24
  • I was concerned that InvariantCulture could interpret the same (ordinally speaking) character in two different ways, thus making two equal objects with different hashcodes. You're saying that's not possible? – Arithmomaniac Jul 13 '12 at 18:17
  • I updated my answer with a link to documentation. I think you should be good. – Joe Castro Jul 13 '12 at 18:21
  • Thanks. I decided to be consistent anyways, and stick with `InvariantCultureIgnoreCase`. – Arithmomaniac Jul 13 '12 at 18:58
  • 1
    Not to mention ... this implementation performs significantly better than anything else you can do. There are many senarios in the apps I work on where this was over 50X better than a ToUpperInvarient().GetHashCode() implemenation in relatively large collection comparisons. There are no string re-allocations and you don't have to deal with ToUpperInvariant vs. ToLowerInvariant differences in languages like Turkish. – Matthew Hazzard Nov 19 '14 at 19:08
8

Now you can use System.HashCode

public class Address
{
    public string ContactName { get; private set; }
    public string Company { get; private set; }
    // ...
    public string Zip { get; private set; }

    public override bool Equals(object obj)
    {
        return
            obj is Address address &&
            string.Equals(ContactName, address.ContactName, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(Company, address.Company, StringComparison.OrdinalIgnoreCase) &&
            // ...
            string.Equals(Zip, address.Zip, StringComparison.OrdinalIgnoreCase);
    }

    public override int GetHashCode()
    {
        var hash = new HashCode();
        hash.Add(ContactName, StringComparer.OrdinalIgnoreCase);
        hash.Add(Company, StringComparer.OrdinalIgnoreCase);
        // ...
        hash.Add(Zip, StringComparer.OrdinalIgnoreCase);
        return hash.ToHashCode();
    }
}
Evil Pigeon
  • 1,887
  • 3
  • 23
  • 31