使用 C# 记录继承 Equals 的实现

Inheriting implementation of Equals with C# record

我正在尝试创建一个基本记录类型,它将使用 Equals() 的不同实现来实现值相等,因为它将使用 SequenceEqual() 来比较集合对象,而不是通过引用来比较它们.

但是,Equals() 的实现并没有像我期望的继承那样工作。

在下面的示例中,我得到了一个派生的 class,它有两个不同的列表。在相等性的默认实现下,这些记录是不同的,因为它是通过引用相等性而不是序列相等性来比较列表。

如果我将基本记录上 Equals() 的默认实现覆盖为始终 return true,单元测试将失败,即使代码正在调用 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));
    }
}

有趣的是,如果我更改实现,使 Equals()DerivedRecord 上实现,而不是在 RecordBase 上实现,单元测试将通过。

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; }
}

此外,此问题特定于记录:如果我将第一个示例的实现更改为使用 classes,则两个实例将评估为相等并且单元测试将通过。

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

因此,在尝试使用记录覆盖 Equals() 的默认实现时,派生记录不会继承基本记录的实现。

这有什么原因吗?直觉上,派生记录似乎应该能够继承基记录对基于值的相等性的实现。但是,我一直在通读 C# 9.0 specification for records,我不确定是否有一个合成的实现可以防止这种情况发生,或者这是否有可能使用记录。

不幸的是,记录的行为方式与您期望的不同。

当您声明一条记录时,您将免费获得相等性检查运算符和方法。

你的基础 class 只是 returns true,但是当你将派生记录声明为 record 时,你也会在那里得到一个相等性检查方法,看起来像这样:

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

由于EqualityComparer<ICollection<int>>.Default.Equals做了一个简单的引用比较,所以这两个列表虽然内容相同,但不认为是相等的,因此你得到false

如果您将类型更改为仅 class 而不是 record,则不会添加编译器生成的 Equals 方法和相关运算符,您只剩下一个从基数 class 开始,即 returns true.

但是对于 record 类型,您将获得针对每个继承级别和类型的方法。这也是为什么直接在派生记录类型上实现它也能根据您的预期“工作”,从那时起编译器就不会为您生成一个。