5

I am trying to create a base record type which will use a different implementation of Equals() for value equality, in that it will compare collection objects using SequenceEqual(), rather than comparing them by reference.

However, the implementation of Equals() doesn't work as I'd expect with inheritance.

In the example below, I have got a derived class which has two different lists. Under the default implementation of equality, these records are different because it is comparing the lists by reference equality, not by sequence equality.

If I override the default implementation of Equals() on the base record to always return true, the unit test will fail, even though the code is calling RecordBase.Equals(RecordBase obj).

public abstract record RecordBase
{
    public virtual bool Equals(RecordBase obj)
    {
        return true;
    }
}

public record DerivedRecord : RecordBase
{
    public DerivedRecord(ICollection<int> testCollection)
    {
        TestCollection = testCollection;
    }

    public ICollection<int> TestCollection { get; init; }
}

public class RecordTests
{
    [Fact]
    public void Equals_WhenCollectionHasSameValues_ReturnsTrue()
    {
        var recordTest1 = new DerivedRecord(new List<int>() { 1, 2, 3 });
        var recordTest2 = new DerivedRecord(new List<int>() { 1, 2, 3 });

        Assert.True(recordTest1.Equals(recordTest2));
    }
}

Interestingly, if I change the implementation so that Equals() is implemented on the DerivedRecord, rather than on RecordBase, the unit test will pass.

public record DerivedRecord : RecordBase
{
    public DerivedRecord(ICollection<int> testCollection)
    {
        TestCollection = testCollection;
    }

    public virtual bool Equals(DerivedRecord obj)
    {
        return true;
    }

    public ICollection<int> TestCollection { get; init; }
}

Furthermore, this issue is specific to records: if I change the implementation of the first example to use classes, the two instances will evaluate to being equal and the unit test will pass.

public abstract class RecordBase
{
    public virtual bool Equals(RecordBase obj)
    {
        return true;
    }
}

So there is something in trying to override the default implementation of Equals() with records, where the derived records will not inherit the base record's implementation.

Is there a reason for this? Intuitively, it seems like a derived record should be able to inherit a base record's implementation of value based equality. However, I've been reading through the C# 9.0 specification for records and I'm not sure if there is a synthesized implementation which is preventing this, or whether this is even possible using records.

7
  • 1
    Hmm. Whenever I override Equals I use 'public override bool Equals', not 'public virtual bool Equals'. Commented Jan 3, 2022 at 13:18
  • ...and the parameter should be (object obj), not a more constrained type. Commented Jan 3, 2022 at 13:20
  • The derived record adds its own check to see if the two list instances are the same (instance). So even though the base class says "I don't care", the derived one cares. This does not happen with pure classes as you won't get a compiler generated Equals method then. Commented Jan 3, 2022 at 13:24
  • 1
    @V0ldek Try using Console.WriteLine instead of Debug.Assert. You can see that it returns false. Commented Jan 3, 2022 at 13:37
  • @LasseV.Karlsen TIL Debug.Assert doesn't work in Fiddle. Commented Jan 3, 2022 at 13:39

1 Answer 1

5

Unfortunately, records don't behave the way you expect them to.

When you declare a record, you get the equality check operator and methods for free.

Your base class just returns true, but when you declare the derived record as a record, you get an equality check method in there as well, that will look like this:

public virtual bool Equals(DerivedRecord other)
{
    return (object)this == other || (base.Equals(other) &&
        EqualityComparer<ICollection<int>>.Default.Equals(
            this.TestCollection,
            other.TestCollection));
}

Since EqualityComparer<ICollection<int>>.Default.Equals does a simple reference comparison, the two lists, though they have the same content, are not considered equal, and thus you get back false.

If you change the types to just class as opposed to record, the compiler-generated Equals method and related operators are not added and you're left with the one from the base class, that returns true.

But for record types, you will get that method for each inheritance level and type. That's also why implementing it directly on the derived record type also "works" according to what you expected, since then the compiler will not generate one for you.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the edit V0ldek, I just grabbed the code as decompiled by LINQPad but I guess it missed that detail. I can see it in the other methods it have decompiled but the Equals method that was shown it missed it.
To comment on the last part of the question: this is value based equality. The semantics this implementation gives you is that records are equal if their types are equal, their base parts are equal according to the base type equality, and the specific parts are equal according to the specific type itself. If you implement your equality by providing DerivedRecord.Equals(DerivedRecord), this will work as expected AND all types inheriting from DerivedRecord will carry this value equality in their synthesised Equals, since they will consult the base type for the properties defined there.
That is of course if you don't provide your own Equals in the deriving types that will work differently, but that's, I think, expected.
Fully agree. I think the expectation here was that the base class Equals method will sort of override the whole system, but that's not how records work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.