Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom EqualityContract in a C# record class

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

like image 338
Bartosz Avatar asked Mar 09 '26 04:03

Bartosz


1 Answers

As far as I understand as of .NET6 you cannot implement mirrored equality for records. You can get true for base1 == derived1 using EqualityContract, as you have shown, but the other way is not possible.

The problem is that while Equals(Derived? other) in Derived will be called it will be called with null for derived1 == base1 and as you rightly point out overriding the other Equals is forbidden.

In my opinion this is not problem because:

  1. Having a derived record that has no extra fields is questionable.
  2. If the records have different flields then compare-only-x behavior would be confusing.

The solution for your issue may a custom equality comparer. (In case you don't know about them, the official EqualityComparer Class has an excellent example with two dictionaries for the same type but with different equality comparers.)

Having:

record Base(int X) {}

record Derived(int X, int Y) : Base(X) {}

class CheckOnlyXComparer : EqualityComparer<Base>
{
    public override bool Equals(Base? b1, Base? b2)
    {
        if (b1 == null && b2 == null) return true;
        if (b1 == null || b2 == null) return false;
        return (b1.X == b2.X);
    }

    public override int GetHashCode(Base b) => b.X.GetHashCode();
}

the following snippet

WriteLine(base1 == derived1);
WriteLine(derived1 == base1);

var comparer = new CheckOnlyXComparer();
WriteLine(comparer.Equals(base1,derived1));
WriteLine(comparer.Equals(derived1,derived1));

prints

False
False
True
True

For

List<Base> l1 = new (){base1, derived1};
WriteLine(l1.Count());
var distinctByX = l1.Distinct(new CheckOnlyXComparer());
WriteLine(distinctByX.Count());

we get

2
1
like image 162
tymtam Avatar answered Mar 10 '26 19:03

tymtam