Record types: overriding EqualityContract breaks the equality/hashcode match

Viewed 2926

Newly introduced record types allow to override EqualityContract for type which makes possible to create a situation when two objects are equal but have different hashcodes which opposes guidelines for GetHashCode overriding:

If you override the GetHashCode method, you should also override Equals, and vice versa. If your overridden Equals method returns true when two objects are tested for equality, your overridden GetHashCode method must return the same value for the two objects.

    public record Base(string Foo);

    public record Child(string Foo, string Bar) : Base(Foo)
    {
        protected override Type EqualityContract => typeof(Base);
    }

    var b = new Base("Foo");
    var c = new Child("Foo", "Bar");
    Console.WriteLine(b == c); // True
    Console.WriteLine(b.GetHashCode() == c.GetHashCode()); // False

While removing the extra property from Child makes GetHashCode and Equals "match".

Obviously this can be fixed with overriding GetHashCode, but I wonder why overriding EqualityContract does not automatically lead to having GetHashCode return value "matching" Equals implementation? Or is there any other way to handle that except manual GetHashCode override?

1 Answers

As you can see in equality members part of records proposal,

GetHashCode() returns an int result of a deterministic function combining the following values:

  • For each instance field fieldN in the record type that is not inherited, the value of System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) where TN is the field type, and

  • If there is a base record type, the value of base.GetHashCode(); otherwise the value of System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract).

So, in your example GetHashCode for Base will be

public override int GetHashCode()
{
    return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) + EqualityComparer<string>.Default.GetHashCode(Foo);
}

And for the Child

public override int GetHashCode()
{
    return base.GetHashCode() + EqualityComparer<string>.Default.GetHashCode(Bar);
}

For the inherited record the EqualityContract isn't used for hash code calculation. If an extra property Bar is removed, the value from Base is used and you'll get a hash values equality. So, overriding of GetHashCode is needed.

This behavior is also observed using sharplab.io

Related