Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Equality check for C# 9 records

From what I understand, records are actually classes that implement their own equality check in a way that your object is value-driven and not reference driven.

In short, for the record Foo that is implemented like so: var foo = new Foo { Value = "foo" } and var bar = new Foo { Value = "foo" }, the foo == bar expression will result in True, even though they have a different reference (ReferenceEquals(foo, bar) // False).

Now with records, even though that in the article posted in .Net Blog, it says:

If you don’t like the default field-by-field comparison behaviour of the generated Equals override, you can write your own instead.

When I tried to place public override bool Equals, or public override int GetHashCode, or public static bool operator ==, and etc. I was getting Member with the same signature is already declared error, so I think that it is a restricted behaviour, which isn't the case with struct objects.

Failing example:

public sealed record SimpleVo
    : IEquatable<SimpleVo>
{
    public bool Equals(SimpleVo other) =>
        throw new System.NotImplementedException();

    public override bool Equals(object obj) =>
        obj is SimpleVo other && Equals(other);

    public override int GetHashCode() =>
        throw new System.NotImplementedException();

    public static bool operator ==(SimpleVo left, SimpleVo right) =>
        left.Equals(right);

    public static bool operator !=(SimpleVo left, SimpleVo right) =>
        !left.Equals(right);
}

Compiler result:

SimpleVo.cs(11,30): error CS0111: Type 'SimpleVo' already defines a member called 'Equals' with the same parameter types

SimpleVo.cs(17,37): error CS0111: Type 'SimpleVo' already defines a member called 'op_Equality' with the same parameter types

SimpleVo.cs(20,37): error CS0111: Type 'SimpleVo' already defines a member called 'op_Inequality' with the same parameter types

My main question here is what if we want to customise the way the equality checker works? I mean, I do understand that this beats the whole purpose of records, but on the other hand, equality checker is not the only feature that makes records cool to use.

One use case where someone would like to override the equality of records is because you could have an attribute that would exclude a property from equality check. Take for example this ValueObject implementation.

Then if you extend this ValueObject abstract class like so:

public sealed class FullNameVo : ValueObject
{
    public FullNameVo(string name, string surname)
    {
        Name    = name;
        Surname = surname;
    }

    [IgnoreMember]
    public string Name { get; }

    public string Surname { get; }

    [IgnoreMember]
    public string FullName => $"{Name} {Surname}";
}

then you would get the following results:

var user1 = new FullNameVo("John", "Doe");
var user2 = new FullNameVo("John", "Doe");
var user3 = new FullNameVo("Jane", "Doe");

Console.WriteLine(user1 == user2); // True
Console.WriteLine(ReferenceEquals(user1, user2)); // False
Console.WriteLine(user1 == user3); // True
Console.WriteLine(user1.Equals(user3)); // True

So far, in order to achieve somehow the above use case, I have implemented an abstract record object and utilise it like so:

public sealed record FullNameVo : ValueObject
{
    [IgnoreMember]
    public string Name;

    public string Surname;

    [IgnoreMember]
    public string FullName => $"{Name} {Surname}";
}

and the results look like this:

var user1 = new FullNameVo
{
    Name    = "John",
    Surname = "Doe"
};

var user2 = new FullNameVo
{
    Name    = "John",
    Surname = "Doe"
};

var user3 = user1 with { Name = "Jane" };

Console.WriteLine(user1 == user2); // True
Console.WriteLine(ReferenceEquals(user1, user2)); // False
Console.WriteLine(user1 == user3); // False
Console.WriteLine(user1.Equals(user3)); // False
Console.WriteLine(ValueObject.EqualityComparer.Equals(user1, user3)); // True

To conclude, I'm a bit puzzled, is restricting the override of equality methods of record objects an expected behaviour or is it because it is still in preview stage? If it is by design, would you implement the above behaviour in a different (better) way or you would just continue using classes?

dotnet --version output: 5.0.100-rc.1.20452.10

like image 625
panosru Avatar asked Oct 12 '20 23:10

panosru


People also ask

How does C check equality?

Checking for equality in C and C++identifier == identifier; The == sign is used to compare primitive types such as char, int, float, etc. The comparison operator returns true if the two identifiers are equal.

What is == in C sharp?

The Equality Operator ( ==) is the comparison operator and the Equals() method in C# is used to compare the content of a string. The Equals() method compares only content.

What is an equality check?

The equality operator ( == ) checks whether its two operands are equal, returning a Boolean result. Unlike the strict equality operator, it attempts to convert and compare operands that are of different types.

Should override Equals C#?

With value types, you should always override the == operator. Like the Equals method, the default implementation of the == operator uses reflection and is slow. Use the same logic as the Equals method, and you'll get much better performance when you're doing equality comparisons on value types.


1 Answers

Per the C#9 record proposal, the following should compile, even if not very useful without actual implementations..

// No explicit IEquatable<R> - this is synthesized!
public sealed record SimpleVo
{
    // Not virtual, as SimpleVo (R) is sealed.
    // Accepts SimpleVo? (R?), and not SimpleVo (R), as argument.
    public bool Equals(SimpleVo? other) =>
        throw new System.NotImplementedException();

    // Optional: warning generated if not supplied when Equals(R?) is user-defined.
    public int GetHashCode() =>
        throw new System.NotImplementedException();

    // No other “standard” equality members!
}

There are restrictions on the equality-related members as most of the code is synthesized. The proposal includes examples of the expected synthesized underlying type.

That is, given just a Equals(R?) the compiler creates a ==, !=, and Equals(object). The methods that can be defined can be found by searching for “user-defined” in the proposal.

Attempting to override/define other equality methods or operators is expected to fail:

It is an error if the override is declared explicitly.

The behavior is discussed in ‘Equality members’ and is summarized in the paragraph:

The record type implements System.IEquatable<R> and includes a synthesized strongly-typed overload of book Equals(R? other) where R is the record type. The method is public, and the method is virtual unless the record type is sealed. The [Equals(R?)] method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or the explicit declaration doesn't allow overriding it in a derived type and the record type is not sealed. If Equals(R? other) is user-defined (not synthesized) but GetHashCode is not [user-defined], a warning is produced.

like image 186
user2864740 Avatar answered Oct 16 '22 22:10

user2864740