Why does EqualityComparer<T>.Default (And thus Array.IndexOf, and thus List.IndexOf) have null special treatment?
#134315
sealed class ReallyComparable : IEquatable<ReallyComparable?> // treats `null` as equal to anything
{
public static bool operator ==(ReallyComparable? one, ReallyComparable? other)
{
if (one is null)
{
return true;
}
else
{
return one.Equals(other);
}
}
public static bool operator !=(ReallyComparable? one, ReallyComparable? other)
=> !(one == other);
public bool Equals(ReallyComparable? other)
=> other is null || ReferenceEquals(this, other);
public override bool Equals(object? obj)
{
if (obj is ReallyComparable other)
{
return Equals(other);
}
else if (obj is null)
{
return true;
}
else
{
return false;
}
}
public override int GetHashCode() => 1;
}var array = Enumerable.Range(0, 5).Select(_ => (ReallyComparable?)new ReallyComparable()).ToArray();
var list = array.ToList();
WriteLine(Array.IndexOf(array, null)); // -1
WriteLine(list.IndexOf(null)); // -1
WriteLine(new ReallyComparable().Equals(null)); // True
WriteLine(new ReallyComparable() == null); // True
WriteLine(EqualityComparer<ReallyComparable?>.Default.Equals(new ReallyComparable(), null)); // FalseAFAIU, the comparer should respect The interesting part is that neither the ops nor methods are even called when trying to find |
Replies: 2 comments 2 replies
|
The current if (x != null)
{
if (y != null) return x.Equals(y);
return false;
}
return y == null;CoreCLR's optimized That is intentional because a non-null value is required to compare unequal to null. In particular,
Those properties are required by hash-based collections and by APIs that use the default equality comparer, so If you intentionally need this non-standard relation, use an explicit comparer with an API that accepts one rather than sealed class ReallyComparer : IEqualityComparer<ReallyComparable?>
{
public bool Equals(ReallyComparable? x, ReallyComparable? y)
{
if (x is null || y is null)
return true;
return ReferenceEquals(x, y);
}
public int GetHashCode(ReallyComparable? obj) => 0;
}Even then, the relation is still non-transitive for distinct non-null instances, so it is unsafe for Sources: |
This is notably an illegal and invalid implementation
In particular (especially note the last bullet):
If you fail to successfully follow these rules, you may encounter undefined behavior, unexpected failures, or other bugs. |
This is notably an illegal and invalid implementation
EqualsandGetHashCode(and by extensionIEquatable<T>.Equals) have implementation contracts you are required to fulfill. See also https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=net-10.0#notes-to-implementers and https://learn.microsoft.com/en-us/dotnet/api/system.object.equals?view=net-10.0#notes-for-inheritorsIn particular (especially note the last bullet):