Given a parent record type:
public record Foo(string Value);
And two record sub-classes Bar
and Bee
I wonder if it is possible to implement Equals
in the base class so as instances of Foo, Bar or Bee are all considered equal based on Value
(both with Equals
and ==
).
I tried the following after digesting https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/records, but it didn't quite work:
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!
}
This question is specific to record types. I don't need an example with classes (I know that already).
When I look at sharplab.io, I see the following implementation for 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);
}
And in 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;
}
So naturally new Bar("foo") == new Bee("foo")
would end-up calling Bar.Equals(Foo?)
, which is synthetic and relies on Bar.Equals(object)
which will cast to Bar
or return null, and because a Bee
is not a Bar
it ends-up comparing null.
It doesn't seem possible to override some of the synthetic Equals methods so it seems I cannot escape this behavior.
Or, can I?
NOTE: I'm on .NET SDK 6.0.
CodePudding user response:
This is the stack trace when calling 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)
According to the draft spec, you cannot declare any of those methods explicitly (i.e. do not have control over them) except Foo.Equals(Foo)
and Bar.Equals(Bar)
, at which point other
has already been (unsuccessfully) casted to Bar
, in Bar.Equals(Object)
.
Though it is not totally impossible - you can declare the operators ==(Bar, Foo)
, !=(Bar, Foo)
etc manually, and let operator overload resolution pick your operator, instead of the ==(Foo, Foo)
one, which you have no control over. But this is quite tedious to do for all the types, and you'd still have the problem of Bar.Equals(Foo)
not working the way you want. :(
EqualityContract
is rather irrelevant. It is only checked in the generated 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 topublic virtual bool Equals(Base? other)
); otherwise the value ofEqualityContract == other.EqualityContract
.
but at that point other
is null already! Not to mention that you wrote your own Foo.Equals(Foo)
, so the EqualityContract
s are not used at all.