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.
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
EqualityContract
, but I couldn't find any example on how can I use it to override the default equality behaviour. – Lychnispublic virtual bool Equals(FullNameVo? other)
. Most other forms ‘expected’ in a normal class type are forbidden — “It is an error if the override is declared explicitly” — from being specified manually defined as they will be synthesized (example given). Iff that works, also add the GetHashCode, as shown. – Blastocoelvirtual
or unseal the FullNamVo type as well: “The record type implements System.IEquatable<R> and includes a synthesized strongly-typed overload of 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 [bool Equals(R? 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.” – BlastocoelIEquatable<R>
does that mean that I don't have to re-implement it? Even withpublic virtual bool Equals(SimpleVo other)
and withoutsealed
keyword, I'm still getting a compiler error. If you have a working example, could you share it? – LychnisValueObject
abstract class in C# 8 here: github.com/panosru/JustDemo/tree/master/ValueObjects/… That is how I have it so far, but I'm testingrecord
types now so since .Net 5 is on RC1 already I feel that I can start testing C# 9 now and migrate to .Net 5 once it is released. – Lychnispublic virtual bool Equals(SimpleVo other)
is notpublic virtual bool Equals(SimpleVo? other)
. Also, per the information in the proposal, removevirtual
if the type issealed
. Also, remove the explicit IEquatable. This is implicit in the record and synthesized, as shown in the synthesized examples. – Blastocoel?
. Based on docs though, it seems impossible to override==
withpublic static bool operator ==(SimpleVo left, SimpleVo right)
or!=
custom operators, right? Am I missing something? – LychnisEquals
then you might not need to override==
and!=
operators (as you used to do with classes and structs), or at least I can't find a case where I would need to override them. Usually, in my code, I just add nullability check inside my custom==
operator. I'm now trying to write a working example and post it since it might help others as well. – Lychnis