skip to content
Stylized letter A in teal with pink sparkle accents Khalid Abuhakmeh

IEqualityComparer in C#: How to Write and Use One

/ 6 min read

Updated:

Equality might seem straightforward, but it tends to get fuzzier the longer you think about it. When it comes to programming, there are two general thoughts around equality.

The first is the idea of precise equality. Given two references to objects, do the two references point to the same location in memory? Programming languages are suited for this kind of comparison, and .NET comes with many prebuilt operators to handle this case.

The second form is logical equality. Anyone who has worked with databases understands the idea of identifiers and their use case for distinguishing sameness. Developers define reasons for equality in code, but the logic can be composed of smaller precise equality comparisons.

In this post, we’ll see how we can implement the IEqualityComparer interface in C# and how it can help us define logical comparisons. Additionally, we’ll see how we can use our implementation in code.

IEqualityComparer<T> in C# is an interface that lets you define custom logical equality rules for a type without changing the type itself, so you can pass alternate “equal means” logic into LINQ methods like Distinct, GroupBy, and Except.

What Is IEqualityComparer in C#?

IEqualityComparer<T> separates “how do I compare two instances” from the type being compared. Instead of baking comparison logic into the class via Equals/GetHashCode overrides or IEquatable<T>, you write a standalone comparer class and hand it to LINQ or collection methods. This is useful when you need different comparison rules in different contexts — for example, comparing Player objects by Username in one place and by Id in another.

The Player Class

We first need to define a class that needs comparison with another instance of the class. For this post, we’ll be using a Player class with two properties.

public class Player
{
public DateTimeOffset LastLogin { get; set; }
= DateTimeOffset.UtcNow;
public string Username { get; set; }
}

Looking at our Player class, we may start to craft a rule for our application.

All players are distinguished by their Username

It seems like a good rule. Let’s see how we can implement the IEqualityComparer to make sure we can work with collections of Player instances.

The Setup

Let’s set up a simple collection with four Player instances, but two unique players.

static void Main(string[] args)
{
var players = new[]
{
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
};
}

As we can see from the code sample, there are two unique players: khalidabuhakmeh and nicoleabuhakmeh. Our next step is to implement the IEqualityComparer. Let’s first look at the interface itself.

namespace System.Collections.Generic
{
public interface IEqualityComparer<in T>
{
bool Equals([AllowNull] T x, [AllowNull] T y);
int GetHashCode([DisallowNull] T obj);
}
}

As we can see, to satisfy the interface, we need to implement two methods: Equals and GetHashCode. Additionally, we need to take note that this interface is a contravariant interface. Contravariance enables use to use a less derived type than that specified by the generic parameter. In our case, we could pass in an object instance instead of a Player instance.

private sealed class PlayerEqualityComparer : IEqualityComparer<Player>
{
public bool Equals(Player x, Player y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return x.Username == y.Username;
}
public int GetHashCode(Player obj)
{
return (obj.Username != null ? obj.Username.GetHashCode() : 0);
}
}

Let’s walk through each line of the Equals method:

  1. If it is the same instance, then it is the same object. Precise equality means logical equality.
  2. If either Player instance is null, then we cannot compare the two usernames. The two players cannot be equal.
  3. Comparing types is optional and depends on your inheritance structure, but in this case, if the two players aren’t the same type, then they are not equal.
  4. Finally, we compare the Username for each player instance.

As a matter of style, we could encapsulate the equality comparer in our Player class and expose the IEqualityComparer through a static property.

public class Player
{
public DateTimeOffset LastLogin { get; set; } = DateTimeOffset.UtcNow;
public string Username { get; set; }
private sealed class PlayerEqualityComparer : IEqualityComparer<Player>
{
public bool Equals(Player x, Player y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return x.Username == y.Username;
}
public int GetHashCode(Player obj)
{
return (obj.Username != null ? obj.Username.GetHashCode() : 0);
}
}
public static IEqualityComparer<Player> Comparer { get; } = new PlayerEqualityComparer();
}

Finally, we can use the equality comparer to find the distinct players in our collection.

class Program
{
static void Main(string[] args)
{
var players = new[]
{
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
};
// using our custom comparer
var unique = players
.Distinct(Player.Comparer)
.ToList();
Console.WriteLine("Unique Players");
unique.ForEach(p =>
{
Console.WriteLine($"- {p.Username} (last seen {p.LastLogin:d})");
});
}
}

We get the following result from running our application.

Terminal window
Unique Players
- khalidabuhakmeh (last seen 04/30/2020)
- nicoleabuhakmeh (last seen 04/30/2020)

Conclusion

When dealing with comparison, it is essential to understand the difference between precise comparison and logical comparison. Luckily for us, .NET has the IEqualityComparer that is used by LINQ. As we saw above, it doesn’t take much code. As our model evolves, we’ll likely have to extend our comparers, so always thinking about what “equal” means now and what it could mean in the future is a valuable exercise.

FAQ

When should I use IEqualityComparer<T> instead of overriding Equals and GetHashCode? Use IEqualityComparer<T> when you need comparison logic that doesn’t belong permanently on the type, or when you need multiple different comparison strategies for the same type. Override Equals/GetHashCode directly when there’s one true definition of equality for that type everywhere it’s used.

What’s the difference between IEqualityComparer<T> and IEquatable<T>? IEquatable<T> is implemented on the type itself and defines a single, intrinsic notion of equality (it also avoids boxing in generic collections). IEqualityComparer<T> is a separate class you pass in, letting you swap comparison strategies without touching the original type.

Do I need to implement GetHashCode if I only call Equals? Yes. LINQ methods like Distinct and GroupBy, along with hash-based collections like Dictionary and HashSet, rely on GetHashCode to bucket items before calling Equals. If your hash codes are inconsistent with your equality logic, lookups and deduplication can quietly return wrong results.

For related equality topics, see the difference between Equals, == operator, and Equals method in C# and converting a C# object to almost any format.

I hope you found this post helpful, and please leave a comment.

Comments

Related Posts