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:

  1. bool Equals(T? x, T? y): Determines whether the specified objects are equal.
  2. int GetHashCode(T obj): Returns a hash code for the specified object.

Crucial Rule: If Equals(x, y) evaluates to true, then GetHashCode(x) and GetHashCode(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: value

Best Practices

  • Immutability: Ensure the properties used in Equals and GetHashCode are 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 Equals and GetHashCode.
  • Null Handling: Always handle null safely inside both interface methods to avoid runtime exceptions.