How to Use IEqualityComparer in C# Dictionaries
This article explains how to implement custom equality comparison
logic for keys in a .NET generic dictionary using the
IEqualityComparer<T> interface. By default,
Dictionary<TKey, TValue> relies on
EqualityComparer<TKey>.Default, which evaluates
standard object equality and hash codes. By implementing a custom
IEqualityComparer<T>, you can control how keys are
compared and hashed, enabling features like case-insensitive string
lookups or equality based on specific properties of complex objects.
The
IEqualityComparer<T> Interface
To create a custom comparer, implement the
System.Collections.Generic.IEqualityComparer<T>
interface. It requires two methods:
bool Equals(T? x, T? y): Determines whether the specified objects are equal.int GetHashCode(T obj): Returns a hash code for the specified object.
Crucial Rule: If
Equals(x, y)evaluates totrue, thenGetHashCode(x)andGetHashCode(y)must return the same value. If this contract is broken, the dictionary cannot locate items correctly.
Step-by-Step Implementation
1. Define the Key Class
Consider a scenario where you have a Product class, and
you want two distinct instances to be treated as identical dictionary
keys if their Id properties match:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}2. Implement
IEqualityComparer<Product>
Create a dedicated class that implements
IEqualityComparer<Product>:
using System;
using System.Collections.Generic;
public class ProductIdComparer : IEqualityComparer<Product>
{
public bool Equals(Product? x, Product? y)
{
// Reference equality and null checks
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
// Business logic equality
return x.Id == y.Id;
}
public int GetHashCode(Product obj)
{
if (obj is null) return 0;
// Hash code must match the property used in Equals
return obj.Id.GetHashCode();
}
}3. Pass the Comparer to the Dictionary Constructor
Instantiate the dictionary and pass an instance of your custom comparer to its constructor:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var inventory = new Dictionary<Product, int>(new ProductIdComparer());
var p1 = new Product { Id = 101, Name = "Keyboard" };
var p2 = new Product { Id = 101, Name = "Mechanical Keyboard" };
inventory[p1] = 50;
// Returns true because p2 has the same Id as p1
bool exists = inventory.ContainsKey(p2);
Console.WriteLine($"Contains p2: {exists}"); // Output: True
// Updates the existing entry for Id 101
inventory[p2] = 75;
Console.WriteLine($"Count: {inventory.Count}"); // Output: 1
Console.WriteLine($"Stock: {inventory[p1]}"); // Output: 75
}
}Built-in Comparers
For standard types like strings, .NET provides pre-built comparers
via the StringComparer class:
// Case-insensitive dictionary lookup
var caseInsensitiveDict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
caseInsensitiveDict["key"] = "value";
Console.WriteLine(caseInsensitiveDict["KEY"]); // Output: valueBest Practices
- Immutability: Ensure the properties used in
EqualsandGetHashCodeare immutable. If a key's hash code changes while it is stored in the dictionary, the dictionary will no longer be able to find it. - Consistency: Always use the exact same properties
in both
EqualsandGetHashCode. - Null Handling: Always handle
nullsafely inside both interface methods to avoid runtime exceptions.