Description
Type of issue
Other (describe below)
Description
The documentation on value equality in C# is not satisfactory for polymorphic class hierarchies.
While there is a disclaimer that the implementation in the class example is not working for the case
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
I believe simply pointing to user to record types is not enough.
If you have a record that has any member that is not implementing value equality (e.g. List), then the problem remains and all of the documentation does not show any safe way how to implement polymorphic equality.
In a way, the definition of IEquatable
is extremely problematic here, as it forces the user to implement an Equals method that will be called based on the declared type.
One way to get around this would be to make the public bool Equals(TwoDPoint p)
virtual, but then we would have to keep overriding it in all the subclasses to get the desired behavior (in a deep hierarchy there would be lot of equals methods to override then...).
Another way that would be possible is to use Explicit Interface Implementation for IEquatable<T>
.
This way we make our hierarchy equatable the way we would like, but bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p)
will not be accessible through the declared type, so public override bool Equals(object? obj)
will be called which is virtual
all the way down with a single signature.
A working example for this can be seen below.
Due to the casting it is also not perfect and can end in an infinite loop, but at least once it's properly implemented it can safely be used.
Looking forward to your thoughts on this approach.
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object? obj) => Equals(obj as TwoDPoint);
bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p) => Equals((object?)p);
protected bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object? obj) => Equals(obj as ThreeDPoint);
bool IEquatable<ThreeDPoint>.Equals(ThreeDPoint? p) => Equals((object?)p);
protected bool Equals(ThreeDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Check properties that this class declares.
if (Z != p.Z)
{
return false;
}
return base.Equals(p);
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}
Page URL
Content source URL
Document Version Independent Id
227a6999-9add-4a0a-98cb-c0c6624d6089
Platform Id
ebf5fc1b-a31e-cddd-874e-c7ac09836f47
Article author
Metadata
- ID: e95b738e-a319-8947-c098-692068c24b9c
- PlatformId: ebf5fc1b-a31e-cddd-874e-c7ac09836f47
- Service: dotnet-csharp
- Sub-service: fundamentals