How to Best Implement Equals for Custom Types

How to best implement Equals for custom types?

There is a whole set of guidelines on MSDN as well. You should read them well, it is both tricky and important.

A few points I found most helpful:

  • Value Types don't have Identity, so in a struct Point you will usually do a member by member compare.

  • Reference Types usually do have identity, and therefore the Equals test usually stops at ReferenceEquals (the default, no need to override). But there are exceptions, like string and your class Point2, where an object has no useful identity and then you override the Equality members to provide your own semantics. In that situation, follow the guidelines to get through the null and other-type cases first.

  • And there are good reasons to keep GethashCode() and operator== in sync as well.

Is it possible to define equality for named types/structs?

No. You can't modify the equality operator and there is no built-in way to add support for custom types to use == syntax. Instead you should compare the pointer values using reflect.DeepEqual.

Go supports equality checking structs.

type Person struct {
Name string
}

a := Person{"Bill DeRose"}
b := Person{"Bill DeRose"}

a == b // true

It won't work with pointer fields (in the way you want) because the pointer addresses are different.

type Person struct {
Friend *Person
}

a := Person{Friend: &Person{}}
b := Person{Friend: &Person{}}

a == b // false

import "reflect"

a := Person{Friend: &Person{}}
b := Person{Friend: &Person{}}

reflect.DeepEqual(a, b) // true

Keep in mind there are caveats.

In general DeepEqual is a recursive relaxation of Go's == operator. However, this idea is impossible to implement without some inconsistency. Specifically, it is possible for a value to be unequal to itself, either because it is of func type (uncomparable in general) or because it is a floating-point NaN value (not equal to itself in floating-point comparison), or because it is an array, struct, or interface containing such a value.

List equality of a custom class

Implementing Equals for a list can be done by using the SequenceEquals method (from System.Linq namespace), which ensures that each item in one list equals the item at the same index in the other list.

One thing you might consider changing, however is your implementation of GetHashCode. This method should return the same number if two items are equal (though it's not guaranteed that two items with the same hash code are equal). Using base.GetHashCode() does not meet this requirement, since the base is object in this case; according to the documentation, "hash codes for reference types are computed by calling the Object.GetHashCode method of the base class, which computes a hash code based on an object's reference", so objects only return the same HashCode if they refer to the exact same object.

The HashCode should be based on the same properties used to determine equality, so in this case we want to use Prop.GetHashCode() for class A, and we want to aggregate the hashcode for all the items in Prop for class B.

Here's one way the classes could be refactored:

public class A : IEquatable<A>
{
public string Prop { get; }

public A(string val)
{
Prop = val;
}

public bool Equals(A other)
{
if (other == null) return false;
return Prop == other.Prop;
}

public override bool Equals(object obj)
{
return Equals(obj as A);
}

public override int GetHashCode()
{
return Prop.GetHashCode();
}
}

public class B : IEquatable<B>
{
public IReadOnlyList<A> Prop { get; }

public B(IReadOnlyList<A> val)
{
Prop = val;
}

public bool Equals(B other)
{
if (other == null) return false;
if (ReferenceEquals(this, other)) return true;
if (Prop == null) return other.Prop == null;
return other.Prop != null && Prop.SequenceEqual(other.Prop);
}

public override bool Equals(object obj)
{
return Equals(obj as B);
}

public override int GetHashCode()
{
return Prop?.Aggregate(17,
(current, item) => current * 17 + item?.GetHashCode() ?? 0)
?? 0;
}
}

C# - What interfaces + operators must be implemented to get value comparison and equality on custom types?

The first step would be to override Object.Equals() and operator== with something like:

No, the first step is to override object.Equals() and GetHashCode(). You must never override one without overriding the other to correspond, or else your class is buggy if used as a key.

Let's look at your Equals()

public override bool Equals(object obj)
{
MyType t = obj as MyType;
return (this.name == t.name) && (this.count == t.count);
}

There's a bug here because if obj is null or is not a MyType this will throw a NullReferenceException. Let's fix that:

public override bool Equals(object obj)
{
MyType t = obj as MyType;
return t != null && (name == t.name) && (count == t.count);
}

I'd also probably put the count comparison before the name as it is likely to give a faster out if it doesn't match, but then I don't know your use-cases so maybe there are a small number of very common count values in which case that doesn't hold. That's an optimisation matter though, let's fix the bug by giving you a corresponding GetHashCode()

public override int GetHashCode()
{
return (name?.GetHashCode() ?? 0) ^ count;
}

The minimum requirement is that if a.Equals(b) then it must be true that a.GetHashCode() == b.GetHashCode(). We also ideally want to spread the bits around as much as possible. We achieve the first (vital) part by basing our hash code on the properties that determine equality. The second part is more complicated, but in this case the relatively good quality of string's GetHashCode() means just xor-ing with the remaining integer value will probably be reasonably good. Search the site for more details (including why just xoring is often not a good idea in other cases).

Now, you want == semantics. It's a requirement that if you define == you must define !=, but we can easily define one in terms of the other.:

public static bool operator !=(MyType x, MyType y)
{
return !(x == y);
}

Now once we've got == done != will go through that. Of course we've already defined equality so we can start with using that:

public static bool operator ==(MyType x, MyType y)
{
return x.Equals(y);
}

This is buggy though because while it handles y being null fine it throws if x is null. We need to consider that too:

public static bool operator ==(MyType x, MyType y)
{
if (x == null)
{
return y == null;
}
return x.Equals(y);
}

Let's consider though that everything must be equal to itself (in fact you will be buggy if that doesn't hold). Since we have to consider the possibility of x == null && y == null let's think about that as an example of the case of (object)x == (object)y. This let's us skip the rest of the testing:

public static bool operator ==(MyType x, MyType y)
{
if ((object)x == (object)y)
{
return true;
}
if ((object)x == null)
{
return false;
}
return x.Equals(y);
}

How much of a benefit this is depends on how likely comparing with self is (it can be more common as a side-effect of various things than you might guess) and how expensive the equality method is (in this case not much, but in a case with more fields to compare it could be considerable).

Okay, we've got the Equals and GetHashCode sorted and we've added a == and != as you wanted them. What would be nice to have is IEqutable<MyType>. This offers a strongly-typed Equals that will be used when available by comparers within dictionaries, hash-sets, etc. So it's a nice to have. This will force us to implement bool Equals(MyType other) which will be much like the override we already did, but without the conversion:

public bool Equals(MyType other)
{
return other != null && (name == other.name) && (count == other.count);
}

Bonus: Because of how overloading works our == is going to call into this slightly faster method instead of the override that does a cast. We've optimised ==, and by extension !=, without even touching them!

Now if we implement this then we have to implement GetHashCode() which in turn means that we have to implement the object.Equals() override, but we've already done that. We're duplicating here though, so lets re-write the override to use the strongly typed form:

public override bool Equals(object obj)
{
return Equals(obj as MyType);
}

All done. Putting it together:

public class MyType : IEquatable<MyType>
{
public string name;
public int count;

public MyType(string n, int c)
{
name = n;
count = c;
}

public bool Equals(MyType other)
{
return other != null && (name == other.name) && (count == other.count);
}

public override bool Equals(object obj) => Equals(obj as MyType);

public override int GetHashCode() => (name?.GetHashCode() ?? 0) ^ count;

public static bool operator ==(MyType x, MyType y)
{
if ((object)x == (object)y)
{
return true;
}

if ((object)x == null)
{
return false;
}

return x.Equals(y);
}

public static bool operator !=(MyType x, MyType y) => !(x == y);
}

IComparable<T> and IComparable are used if you also want to be able to order your objects; to say one is less than or comes before the other. It isn't needed for equality.

IEqualityComparer<T> and IEqualityComparer are used to override all the above and define equality in some completely different way in another class. The classic example here is that sometimes we want "abc" to be equal to "ABC" and sometimes we don't, so we can't just depend on == or the Equals() methods of the types we've described above as they can only apply one rule. They are generally provided by other classes to the actual class being compared.

Let's say we sometimes want to ignore case in comparing MyType instances. Then we can do:

public class CaseInsensitiveMyTypeEqualityComparer : IEqualityComparer<MyType>
{
public bool Equals(MyType x, MyType y)
{
if ((object)x == (object)y)
{
return true;
}
if ((object)x == null | (object)y == null)
{
return false;
}
return x.count == y.count && string.Equals(x.name, y.name, StringComparison.OrdinalIgnoreCase);
}

public int GetHashCode(MyType obj)
{
if (obj == null)
{
return 0;
}
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.name) ^ obj.count;
}
}

If you used this with say:

var dictionary = new Dictionar<MyType, int>(new CaseInsensitiveMyTypeEqualityComparer());

Then the dictionary would be case-insensitive for its keys. Note that since we defined equality based on case-insensitive comparison of the names we have to base the hash code on a case-insensitive hashing of it too.

If you don't use an IEqualityComparer<MyType> then the dictionary uses EqualityComparer<MyType>.Default which uses your more-efficient IEquatable<MyType> implementation since it can, and would have used the object.Equals override if you didn't have that.

You might guess that IEqualityComparer<T> is relatively less-used than just using the equality defined by a class itself. Also if someone does need it, that person might not be you; one of the great things about it is that we can define them for other people's classes. Still, it's not a concern for your design of your class itself.

Custom Equality check for C# 9 records

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.

ICollectionT.Contains on custom types

Because your type doesn't override Equals, the default implementation of Equals will be used, i.e. reference equality. So Contains will be true if the collection contains that very instance.

To use your own comparison, implement IEqualityComparer<T> (e.g. to compare the Ids) and pass an instance of your comparer into the Contains method. (This assumes you are able to use LINQ extensions, as the "native" ICollection<T>.Contains method doesn't have the IEqualityComparer overload.)

What needs to be overridden in a struct to ensure equality operates properly?

An example from msdn

public struct Complex 
{
double re, im;
public override bool Equals(Object obj)
{
return obj is Complex c && this == c;
}
public override int GetHashCode()
{
return re.GetHashCode() ^ im.GetHashCode();
}
public static bool operator ==(Complex x, Complex y)
{
return x.re == y.re && x.im == y.im;
}
public static bool operator !=(Complex x, Complex y)
{
return !(x == y);
}
}

Correct way to override Equals() and GetHashCode()

You can override Equals() and GetHashCode() on your class like this:

public override bool Equals(object obj)
{
var item = obj as RecommendationDTO;

if (item == null)
{
return false;
}

return this.RecommendationId.Equals(item.RecommendationId);
}

public override int GetHashCode()
{
return this.RecommendationId.GetHashCode();
}


Related Topics



Leave a reply



Submit