Home > other >  Equality of interface types implemented by records
Equality of interface types implemented by records

Time:10-06

Let's say I got the record

public sealed record Person(int Id, string GivenName, string Surname)
{
  // imagine a few non-trivial methods here
}

I got other code that deals with Persons that I want to unit-test. Because I don't want to have to construct exactly the right instances to make the methods return what the tests need, I introduce an interface:

public interface IEntity
{
  int Id { get; }
  // pulled up all the non-trivial methods from Person
}

and add IEntity to the interface implementation list of Person.

However, somewhere in code depending on IEntitys, I need to check two instances for equality:

[Test]
public void Repro()
{
    // arrange
    IEntity lhs = new Person(1, "John", "Doe");
    IEntity rhs = new Person(1, "John", "Doe");
    // act
    var result = lhs == rhs;
    // assert
    result.Should().BeTrue();
}

The assertion fails. Because while Person implicitly defines an operator==, there is none for IEntity, so the default implementation is used which in turn relies on object.Equals() which uses reference equality.

And there is no way for me to define that missing operator==(IEntity, IEntity):

In Person, I cannot define it because at least one of the two operands must be Person.

In IEntity, I cannot define it because, well, I cannot implement static methods (even with C#11 I could only define it abstractly).

So that makes implementing interfaces with records rather dangerous because the intuitive assumption one would make is not correct. Which leaves only not using an interface, but that again makes writing tests for code depending on the behavior of Person very tedious.

Am I overlooking something here? Or how else can I abstract Person such that it makes testing dependent code easy while not introducing that subtle issue?

CodePudding user response:

A few points.

the default implementation is used which in turn relies on object.Equals() which uses reference equality.

This is not correct. Reference equality is checked with object.ReferenceEquals

Per the documentation,

It is an error if the [==, !=] operators are declared explicitly.

And the same for Equals.

I think your question is more or less a duplicate of this one which says it's not possible to do what you want.

Having said that, why can't you call .Equals, which seems to do what you want?

public sealed record Person(int Id, string GivenName, string Surname) : IEntity
{
}

public interface IEntity
{
    int Id { get; }
}

internal class Program
{
    static void Main(string[] args)
    {
        IEntity lhs = new Person(1, "John", "Doe");
        IEntity rhs = new Person(1, "John", "Doe");

        var result = lhs == rhs;
        var resultEquals = object.Equals(lhs, rhs);
        var resultRefEquals = object.ReferenceEquals(lhs, rhs);

        Console.WriteLine($"result: {result}, resultEquals: {resultEquals}, resultRefEquals: {resultRefEquals}");
    }
}

console out:

result: False, resultEquals: True, resultRefEquals: False

CodePudding user response:

As @Sweeper pointed out correctly in the comments, there is currently no good way to specify for an interface how the equality operator should behave.

So my solution is to make IEntity extend IEquatable<IEntity> and depend on the .Equals()method from that interface - the idea being that if someone sees an interface implementing IEquatable<>, they know what to do.

I guess in the end my feeling of discontent stems really just from the fact that there is no way to specify in the interface that operator== and its inverse are to work per-member and having that requirement automatically fulfilled by a record.

  • Related