Introduction
It’s essential to know how to override the Equals
and GetHashCode
methods to properly define the equality of types we create. In this article, we’ll look at how to implement these methods, why IEquatable
interface is useful and how Tuples introduced in C# 7.0 can make our implementation elegant.
Basic Implementation
To understand the importance of Equals
and GetHashCode
methods, let’s start with reference types and create a simple class Person
:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
After that, run a simple test:
void Main()
{
var person1 = new Person { Id = 1, Name = "Eric" };
var person2 = new Person { Id = 1, Name = "Eric" };
var personList = new List<Person> { person1 };
var personDictionary = new Dictionary<Person, int> { { person1, 1 } };
Console.WriteLine($"person1.Equals(person2) = { person1.Equals(person2) }");
Console.WriteLine($"personList.Contains(person2) = { personList.Contains(person2) }");
Console.WriteLine($"personDictionary.ContainsKey(person2) =
{ personDictionary.ContainsKey(person2) }");
}
Note that by default, our Person
objects are compared by their reference values. Since person1
and person2
are not the same object and they have different references, they are not considered equal. The result is:
In order to set rules for a proper equality comparison, we need to override the Object.Equals
method.
Note: If we just implement a method named Equals
without the override
keyword, it will just be an ordinary method. It will work for the first line of the test where Equals
is called explicitly, but all the other lines will still be returning False
. Let’s add the following code to our Person
class:
public override bool Equals(object obj)
{
var person = obj as Person;
if (person == null)
return false;
return person.Id == Id && person.Name == Name;
}
Now, if we run the same test again, we get the result:
Note that the overridden Equals
method works well for the List.Contains
(2nd line), but Dictionary.ContainsKey
still returns False
(3rd line). It’s because such containers as HashSet
or Dictionary
use a hash function to compare objects. If we execute the following code, we will notice that currently person1
and person2
have different hash values:
Console.WriteLine(person1.GetHashCode());
Console.WriteLine(person2.GetHashCode());
Let’s override GetHashCode
method in the Person
class as well:
public override int GetHashCode() =>
new { Id, Name }.GetHashCode();
Now person1
and person2
have the same hash values (if values of their properties have same values) and Dictionary.ContainsKey
is returning True
as well!
Value Types and IEquatable
It’s not necessary to override Object.Equals
method for value types if we are satisfied by the default comparison logic happening behind the scenes through reflection. However, the reflection is leading to poor performance and it’s recommended to explicitly override Equals
method. Note that the Object.Equals
accepts object
data type as a parameter. It’s not beneficial for value types since boxing occurs when value types are being casted to object
and it results in poor performance. That’s where implementation of the IEquatable
interface becomes helpful. The interface has only one method: public bool Equals(T other)
. The difference between Object.Equals
and IEquatable.Equals
is the data type of the input parameter. IEquatable
allows passing a strongly typed parameter and it resolves the problem with boxing which happens when Object.Equals
is used.
It’s considered as a good practice to override GetHashCode
method along with Equals
and it’s better than relying on the implicit logic of this method. Let’s replace the class
keyword with struct
for the Person
and implement IEquatable
interface:
public struct Person : IEquatable<Person>
{
public int Id { get; set; }
public string Name { get; set; }
public bool Equals(Person person) =>
person.Id == Id && person.Name == Name;
public override int GetHashCode() =>
new { Id, Name }.GetHashCode();
}
If we run the same test for our structure, we get these results:
Reference types don’t benefit from implementation of IEquatable
as much as value types, but some developers still like to implement it for consistency along with overriding Object.Equals
method.
Replacing Code with Tuples
Now, let's consider the main point which motivated me to write this article. We can use Tuples introduced in C# 7.0 to replace the tedious pairwise comaprison of each field in the Equals
method! We can make it significantly shorter:
public bool Equals(Person person) =>
(Id, Name).Equals((person.Id, person.Name));
Also, we can modify GetHashCode
method:
public override int GetHashCode() =>
(Id, Name).GetHashCode();
Recap and Final Code
Both, Object.Equals
and GetHashCode
methods should be overridden for reference types together in order to get correct results of equality comparison. Performance of value types benefits from implementation of IEquatable
interface because of the strongly typed parameter its Equals
method has. Overriding the GetHashCode
method for value types helps explicitly define the hash equality rules and avoid relying on the implicit logic. For consistency, many developers prefer to explicitly override Object.Equals
and GetHashCode
regardless of type (either value type or reference type); also, they implement IEquatable
interface. See the final code of the Person
structure/class including overloads for ==
and !=
operators:
public struct Person : IEquatable<Person>
{
public int Id { get; set; }
public string Name { get; set; }
public bool Equals(Person person) =>
(!object.ReferenceEquals(person, null)) &&
(Id, Name).Equals((person.Id, person.Name));
public override bool Equals(object obj) =>
(obj is Person) && Equals((Person) obj);
public override int GetHashCode() =>
(Id, Name).GetHashCode();
public static bool operator ==(Person p1, Person p2) =>
(!object.ReferenceEquals(p1, null)) && p1.Equals(p2);
public static bool operator !=(Person p1, Person p2) =>
!(p1 == p2);
}
See the source code on GitHub.