From the spec:
The record type implements
System.IEquatable<R>
and includes a synthesized strongly-typed overload ofEquals(R? other)
whereR
is the record type. The method ispublic
, and the method isvirtual
unless the record type issealed
.
What is the real purpose of having the method declared as virtual
when we can't really override it with user code in derived classes since it's auto-implemented by the compiler? What does the compiler's auto-generated implementation guarantee for records to work well that we couldn't live without otherwise?
return Equals((object)other);
Sweeper's answer is in sync with the current official docs which describe value equality:
For types with the record modifier (record class, record struct, and readonly record struct), two objects are equal if they are of the same type and store the same values.
that is however
If you don't override or replace equality methods, the type you declare governs how equality is defined:
So, modifying slightly Sweeper's example by overriding the virtual property EqualityContract
:
void Main() {
Compare(
new A() { Foo = "a"},
new B() { Foo = "a", Bar = "c" }
);
}
public record A {
public string Foo { get; set; } = "";
}
public record B : A {
public string Bar { get; set; } = "";
protected override Type EqualityContract => base.EqualityContract;
}
void Compare(A a, A b) {
Console.WriteLine(a.Equals(b)); // True
}
Here only the Foo
part is evaluated between the two objects and it returns true. b.Equals(a)
returns false though which is a bit confusing as evidenced ( and discussed in detail) in this and this questions.
My best attempt at conceptualizing the feature is that it just lets us extend the concept of same type (invariant) to be a type assignable to (covariant).
For example:
public record A(string FirstName) {}
public record B(string FirstName, string LastName) : A(FirstName) {
protected override Type EqualityContract => base.EqualityContract;
}
public record C(string FirstName, string LastName, int Age) : B(FirstName, LastName) {
protected override Type EqualityContract => base.EqualityContract;
}
var aRecord = new A("John");
var bRecord = new B("John", "Doe");
var cRecord = new C("John", "Doe", 33);
aRecord.Equals(cRecord).Dump(); // True -> C is assignable to A
aRecord.Equals(bRecord).Dump(); // True -> B is assignable to A
bRecord.Equals(cRecord).Dump(); // True -> C is assignable to B
cRecord.Equals(aRecord).Dump(); // False -> A is NOT assignable to C
This form of covariance works only from the actual object's runtime type, and not from the variable reference type, i.e.
A cRecord = ...
cRecord.Equals(aRecord).Dump() // will evalute to false
In my opinion, this has to do with the original implementation (following shortly) that relied on the virtual dispatch of object.Equals
.
The most canonical explanation I could find for what EqualityContract
is supposed to enable is from March 2020 when the typed Equals(R obj
still hadn't been decided on and they used object.Equals
for examples:
public abstract class Person // Root of hierarchy with value equality
{
public string Name { get; set; }
protected virtual Type EqualityContract => typeof(Person);
public override bool Equals(object other) =>
other is Person that
&& object.Equals(this.EqualityContract, that.EqualityContract)
&& object.Equals(this.Name, that.Name);
}
public class Student : Person // derived class
{
public int ID { get; set; }
protected override Type EqualityContract => typeof(Student);
public override bool Equals(object other) =>
base.Equals(other) // checks EqualityContract and Name
&& other is Student that // if other is just Person won't work
&& object.Equals(this.ID, that.ID);
}
There was some discussion in the issue as why not implement the type check in the base class with an approach similar to this.GetType()==other.GetType()
which would have worked in preventing the base class for evaluating ONLY its state against an instance of a derived class and mistakenly returning true. The main response was:
Some people feel this is a niche scenario, and the compiler should just generate a typeof(...) check directly. I don't know. I like solving the whole equality problem of hierarchies. Having two values agree that they have the same equality contract seems like it fully addresses symmetry, while respecting the choice of derived classes ("I want to be like my parent" vs "I am my own kind of thing"). I think this approach (originally proposed by @gafter long ago) is super elegant.
In June 2020 they had drafted typed Equals(R obj)
for inheritance scenarios that have functionally the same behavior as today. I couldn't really find in all the meeting notes why they decided to make the methods effectively sealed virtual
(you cannot override the methods in derived classes legally).
If we didn't have the EqualityContract
feature and we just compared for exact type in the root record Equals(no user option for assignable/covariance), those methods could have really just been instance methods and not virtual. Using the example with A
, B
, C
from above.
A aRecord = new A...
A bRecord = new B...
bRecord.Equals(aRecord);
// this would use the A.Equals which could
// have just checked for exact type match (between this and other) and returned false.
But with the EqualityContract
overridable check in the root's A.Equals
we could have it evaluate to true. A is not assignable to B, as B was to A from the first example, so that's no longer the original (already confusing to some) covariant solution. So they had to prevent this.
The way they did is with having the method be virtual
so to trigger virtual dispatch where they could forward to a type checking obj.Equals to prevent the behavior:
// B's override of A.Equals that we cannot generate ourselves
public sealed override bool Equals(A other)
{
return Equals((object)other); // let's say we have A object
}
// object.Equals that we cannot generate ourselves
public override bool Equals(object obj)
{
// effectively preventing us from comparing
// objects that are not assignable to B from
// comparing to B at all
return Equals(obj as B); // will evaluate to null -> false
}