What is the proper way and the intended use of providing a custom implementation for the System.Type EqualityContract { get; } property in a C# record class?
The default (synthesized) implementation returns typeof(R) for a record class R. Because of that, instances of type R can be compared only with other instances of type R, otherwise the result is always false. This makes perfect sense in typical cases. However, replacing that default implementation by a custom one is allowed, so there must be a reason for that. I would expect it is to allow several record classes to share the equality contract, e.g., if they have the same instance properties or they derive from the same base record class. However, this would require declaring custom overrides of bool Equals(object? other) and bool Equals(Base? other), which is forbidden.
[EDIT: Note that the question is about the purpose and usability of defining a custom equality contract. The example below is only to show a potential use which however does not work. I am not asking how to make the example work using a different mechanism.]
For example, suppose I want a record class Derived to reuse the equality contract of its base record class Base in order to allow comparisons between instances of Base and Derived based on the instance properties of Base. I would need to do something like this:
record Base(int X);
record Derived(int X) : Base(X) {
protected override System.Type EqualityContract => base.EqualityContract;
public virtual bool Equals(Derived? other) => base.Equals(other);
public sealed override bool Equals(Base? other) => base.Equals(other); // forbidden
public override bool Equals(object? other) => base.Equals(other); // forbidden
/* some additional stuff */
}
I cannot do that because declaring custom overrides of Equals is forbidden in C# records. Without the forbidden custom overrides, the record class Derived looks as follows (with synthesized overrides of Equals included for illustration):
record Derived(int X) : Base(X) {
protected override System.Type EqualityContract => base.EqualityContract;
public virtual bool Equals(Derived? other) => base.Equals(other);
public sealed override bool Equals(Base? other) => Equals((object)other); // synthesized
public override bool Equals(object? other) => Equals(other as Derived); // synthesized
/* some additional stuff */
}
Then the equality contract does not work correctly. For example, the following code produces True False:
Base obj1 = new Base(1);
Derived obj2 = new Derived(1);
System.Console.WriteLine($"{obj1 == obj2} {obj2 == obj1}");
Both comparisons use the operator == of Base, which calls the method bool Equals(Base? other). For obj1 == obj2, the implementation from Base is used, which compares the equality contracts (equal in this case) and the values of X (equal in this case) and returns true. For obj2 == obj1, the implementation from Derived is used, which results in a call to obj2.Equals(obj1 as Derived), which returns false, because obj1 as Derived is null.
So how am I supposed to make use of custom equality contracts? Is there anything I am missing? What would be wrong with allowing custom overrides to Equals?
I have searched through the discussions in the C# design repository to find some information. The only two relevant comments I have found indicate that my scenario above was intended to work: https://github.com/dotnet/csharplang/issues/3137#issuecomment-581558013 https://github.com/dotnet/csharplang/discussions/3787#discussioncomment-130523