It's highly recommended to not simply add specialized Equals
overloads to your value type.
Always implement IEquatable<T>
.
Implementing IEquatable<T>
also enables other behavioral improvements as this interface is heavily used by other .NET types like generic collections.
For example, methods like Contains, Remove or IndexOf all rely on IEquatable<T>
. Same applies to EqualityComparer<T>.Default
references in general (EqualityComparer<T>.Default
is also internally used in collections).
If you want your value type to behave properly when used in a common manner (e.g., in collections and tables like Dictionayr<K, V>
) you must implement IEquatable<T>
.
The default equality operator definition checks for reference equality (compiler level). This means if the instances are the same, or in other words if two variables/operands point to the same memory object, the ==
operator returns true
.
Because value types are always copied by value, the variable will never point to the same memory object (as copying by value means the creatin of a new memory object with a copy of each value of the old memory object).
Equality of a value type is therefore defined by comparing the values of an instance (value equality).
In this context it should be noted that it is not recommended to use object.Equals
to compare for equality.
Although ValueType
(the base class of e.g., struct
or enum
) overrides object.Equals
to perform a basic value comparison for the passed in value type, object.Equals
performs very bad:
- because the parameter is defined as being of type
object
there incurs the cost of boxing (conversion of a value type to a reference type).
- because the equality comparison is as general as possible, the override must use reflection to get all fields - of both participating value types (the current and the instance to compare to) to compare their values one by one.
There is no default equality operator overload for value types. Such an overload must be very slow as it had to use reflection to get all field values of a type for equality comparison (like the override of object.Equals
provided by ValueType
does). Given the frequency of how often the equality operator is used it makes perfect sense to let each value type define its own specialized equality comparison. This eliminates boxing and the use of reflection.
For this reason, it is recommended best practice to always let your value type implement IEquatable<T>
if it qualifies for equality comparison.
This solution also avoids the expensive boxing conversion as the IEquatable<T>.Equals
is now able to accept the concrete value type as argument.
When implementing IEquatable<T>
it's also best practice to override object.Equals
, object.GetHashCode
(those overrides always go in tandem) and to overload the equality and inequality operators.
This way consistent behavior is guaranteed across the API.
Note: in the wake of overriding the default object
behavior it's also best practice to override object.ToString
.
An example implementation of IEquatable<T>
to enable all equality comparison features could look as follows:
User.cs
public readonly struct User : IEquatable<User>
{
public Guid UserGuid { get; }
public string Username { get; }
public User(Guid userGuid, string username)
{
this.UserGuid = userGuid;
this.Username = username;
}
#region IEquatable<User> implementation
// Use ValueTuple to simplify the equality comparison implementation
public bool Equals(User other)
=> (other.UserGuid, other.Username) == (this.UserGuid, this. Username);
#endregion IEquatable<User> implementation
#region object overrides
public override bool Equals(object obj)
=> obj is User other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine(this.UserGuid, this. Username);
// For the sake of completeness
public override string ToString()
=> $"User: {this. Username} (ID: {this.UserGuid})";
#endregion object overrides
#region Operator overloads
public static bool operator ==(User left, User right)
=> left.Equals(right);
public static bool operator !=(User left, User right)
=> !(left == right);
#endregion Operator overloads
}
Usage example
User user = new User();
// Using the equality operator
bool isDefault = user == default;
// Using the IEquatable.Equals method
bool isDefault = user.Equals(default);
==
operator would just do a reference comparison on them. – Edge