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.
CodePudding user response:
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.
