Skip to content

[Bug]: Mutability of record GetHashCode #9595

Description

@YakudzaBY

Type of issue

Spec incorrect

Description

Specification states that record GetHashCode uses only positional properties (i.e. the ones specified in default constructor).
But in current implementation it uses at least public properties as well. Here is the failing test

namespace RecordGetHashCodeMutability;

public record A(int X)
{
    public int Y { get; set; }
}

public record B(int X)
{
    public int Y { get; set; }

    public override int GetHashCode() => X.GetHashCode();
}

[TestClass]
public sealed class Test1
{
    [TestMethod]
    public void Given_Record_When_Modify_Non_Positional_Property_Then_HashCode_Shouldnt_Change()
    {
        var a = new A(default);
        var hashCode1 = a.GetHashCode();
        a.Y = 2;
        var hashCode2 = a.GetHashCode();
        Assert.AreEqual(hashCode1, hashCode2, "Hash codes should be equal even after mutating the record's non-positional property.");
    }

    [TestMethod]
    public void Given_Record_With_Overriden_GetHashCode_When_Modify_Non_Positional_Property_Then_HashCode_Shouldnt_Change()
    {
        var b = new B(default);
        var hashCode1 = b.GetHashCode();
        b.Y = 2;
        var hashCode2 = b.GetHashCode();
        Assert.AreEqual(hashCode1, hashCode2, "Hash codes should be equal even after mutating the record's non-positional property.");
    }

    [TestMethod]
    public void Given_Dictionary_Containing_Record_When_Modifying_Non_Positional_Property_Then_Should_Still_Contains_Key()
    {
        var a = new A(default);
        var d = new Dictionary<A, int>
        {
            [a] = 1
        };
        Assert.IsTrue(d.ContainsKey(a), "Dictionary should contain the key after adding it.");
        a.Y = 2;
        Assert.IsTrue(d.ContainsKey(a), "Dictionary should still contain the key after modifying the non-positional property.");
    }
}

Page URL

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/records.md#equality-members

Content source URL

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/records.md#equality-members

Activity

  1. YakudzaBY commented on Aug 8, 2025

    @YakudzaBY
    Author

    I had to add Page Urls and Content source Urls, as it wouldn't allow create issue with out it. Even though Issue creating form says that they would be autofilled.

  2. huoyaoyuan commented on Aug 8, 2025

    @huoyaoyuan
    Member

    Record hasn't been specificized at all. The specification is still a draft at dotnet/csharpstandard#983 .
    The linked document you are referencing is under the "rejected" folder. It isn't the version that serves for implementation. The accepted version is at https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/records.md .

  3. YakudzaBY commented on Aug 8, 2025

    @YakudzaBY
    Author

    Record hasn't been specificized at all. The specification is still a draft at dotnet/csharpstandard#983 . The linked document you are referencing is under the "rejected" folder. It isn't the version that serves for implementation. The accepted version is at https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/records.md .

    Thank you, updated links, though question still remains.

  4. colejohnson66 commented on Aug 8, 2025

    @colejohnson66

    This is indeed confusing, but the spec/proposal allows it:

    The synthesized override of GetHashCode() returns an int result of 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).

    Since properties are backed by fields, point 1 applies.

    IMHO, there should be a compiler-generated warning saying you should implement GetHashCode() manually.

  5. CyrusNajmabadi commented on Aug 8, 2025

    @CyrusNajmabadi
    Contributor

    Specification states that record GetHashCode uses only positional properties (i.e. the ones specified in default constructor).
    But in current implementation it uses at least public properties as well. Here is the failing test

    No. Records use all backing fields in the type for equals/gethashcode. This is the design we wanted (as these represent value types, and all fields in a record are part of its value), and what hte impl does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions