C# 记录类型:记录子类 之间的相等比较

C# Record Types: Equality comparisons between record sub-classes

给定父记录类型:

public record Foo(string Value);

和两个记录子 classes BarBee 我想知道是否可以在基础 class 中实现 Equals 以便Foo、Bar 或 Bee 的实例都被认为是 相等 基于 Value(两者都具有 Equals==)。

我在消化 https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/records 后尝试了以下方法,但效果不佳:

    public record Foo(string Value)
    {
        public virtual bool Equals(Foo? other)
        {
            return other != null && this.Value == other.Value;
        }

        public override int GetHashCode() => this.Value.GetHashCode();
    }

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

    public record Bee(string Value) : Foo(Value)
    {
        protected override Type EqualityContract => typeof(Foo);
    }

    [Test]
    public void TestFooBar()
    {
        Assert.That(new Foo("foo") == new Bar("foo"), Is.True); // Passes
        Assert.That(new Bar("foo") == new Foo("foo"), Is.True); // Fails!
    }

    [Test]
    public void TestFooBee()
    {
        Assert.That(new Foo("foo") == new Bee("foo"), Is.True); // Passes
        Assert.That(new Bee("foo") == new Foo("foo"), Is.True); // Fails!
    }

    [Test]
    public void TestBarBee()
    {
        Assert.That(new Bar("foo") == new Bee("foo"), Is.True); // Fails!
        Assert.That(new Bee("foo") == new Bar("foo"), Is.True); // Fails!
    }

此问题特定于记录类型。我不需要 classes 的示例(我已经知道了)。

当我查看 sharplab.io 时,我看到 Bar 的以下实现:

    [System.Runtime.CompilerServices.NullableContext(2)]
    public override bool Equals(object obj)
    {
        return Equals(obj as Bar);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public sealed override bool Equals(Foo other)
    {
        return Equals((object)other);
    }

并且在 Foo 中:

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator ==(Foo left, Foo right)
    {
        if ((object)left != right)
        {
            if ((object)left != null)
            {
                return left.Equals(right);
            }
            return false;
        }
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public override bool Equals(object obj)
    {
        return Equals(obj as Foo);
    }
    
    [System.Runtime.CompilerServices.NullableContext(2)]
    public virtual bool Equals(Foo other)
    {
        if (other != null)
        {
            return Value == other.Value;
        }
        return false;
    }

所以自然地 new Bar("foo") == new Bee("foo") 最终会调用 Bar.Equals(Foo?),它是合成的并且依赖于 Bar.Equals(object),它将转换为 Bar 或 return null,并且因为 Bee 不是 Bar 它最终比较 null.

似乎无法覆盖某些合成的 Equals 方法,因此我似乎无法避免这种行为。

或者,我可以吗?

注意:我使用的是 .NET SDK 6.0。

这是调用 new Bar("foo") == new Foo("foo"):

时的堆栈跟踪
at Foo.Equals(Foo other)
at Bar.Equals(Bar other)
at Bar.Equals(Object obj)
at Bar.Equals(Foo other)
at Foo.op_Equality(Foo r1, Foo r2)

根据 draft spec,除了 Foo.Equals(Foo)Bar.Equals(Bar),您不能显式声明任何这些方法(即无法控制它们),此时 other 已经(未成功)转换为 Bar,在 Bar.Equals(Object).

虽然这并非完全不可能 - 您可以手动声明运算符 ==(Bar, Foo)!=(Bar, Foo) 等,并让运算符重载决策选择您的运算符,而不是 ==(Foo, Foo) 一个,你无法控制的。但这对于所有类型来说都非常乏味,而且您仍然会遇到 Bar.Equals(Foo) 无法按您想要的方式工作的问题。 :(

EqualityContract 是无关紧要的。仅在生成的 Foo.Equals(Foo),

中检查

The synthesized Equals(R?) returns true if and only if each of the following are true:

  • [...]
  • If there is a base record type, the value of base.Equals(other) (a non-virtual call to public virtual bool Equals(Base? other)); otherwise the value of EqualityContract == other.EqualityContract.

但此时 other 已经为空!更何况你写了自己的 Foo.Equals(Foo),所以 EqualityContract 根本没有用。