Skip to Content
Course content

242: Whiteboard Practice: Implementing a Custom Equality Comparer

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a warehouse inventory system. He had a list of Product objects and was using .Distinct() to ensure he didn't process the same item twice. He was pulling his hair out because the code was letting duplicates slide right through, even though the Product IDs were identical. The problem? He was using a class, and C# was performing reference equality. To the runtime, two different objects in different memory addresses are different, even if every single property inside them is identical. He couldn't just override Equals in the Product class because that class lived in a shared library used by three other teams, and changing the fundamental definition of "equality" for everyone would have caused a production meltdown.

This is where the IEqualityComparer<T> interface becomes your best friend. It allows you to define equality logic externally. You aren't changing the object itself; you're providing a specialized "rulebook" that you can hand to a collection like a HashSet or a Dictionary to tell it how to judge two objects.

The Contract of Equals and GetHashCode

When you implement IEqualityComparer<T>, you have to fulfill two requirements: Equals and GetHashCode. I cannot stress this enough—you cannot do one without the other. If you only implement Equals, your comparer will work for a List, but it will fail miserably in a Dictionary or HashSet. These collections use the hash code as a "bucket" index to find objects quickly. If two objects are "equal" but have different hash codes, the collection will look in the wrong bucket and conclude the object doesn't exist.

public class ProductComparer : IEqualityComparer<Product>
{
    public bool Equals(Product x, Product y)
    {
        // Handle nulls first to avoid NullReferenceExceptions
        if (ReferenceEquals(x, y)) return true;
        if (x == null || y == null) return false;

        // We define equality based on the SKU (Stock Keeping Unit)
        return x.SKU == y.SKU;
    }

    public int GetHashCode(Product obj)
    {
        if (obj == null) return 0;
        
        // Use HashCode.Combine for a robust, well-distributed hash
        return HashCode.Combine(obj.SKU);
    }
}

I used HashCode.Combine here because it's the modern way to do it. In older versions of C#, you'd see a lot of XORing (^) and prime number multiplication, which is a headache to get right. Trust the built-in utility.

Plugging the Comparer into Collections

The magic happens when you instantiate your collection. Most generic collections in System.Collections.Generic have a constructor overload that accepts an IEqualityComparer. Once you pass your custom comparer in, the collection stops using the default reference equality and starts using your logic.

If you're using LINQ methods like Distinct() or Intersect(), you can pass the comparer directly as an argument. This is incredibly powerful because it means you can have multiple different comparers for the same object—one that compares by SKU for inventory, and another that compares by Category for reporting—without ever touching the Product class itself.

var products = new List<Product> { /* ... data ... */ };
var comparer = new ProductComparer();

// This now uses our SKU logic instead of memory addresses
var uniqueProducts = products.Distinct(comparer).ToList();

// Or for a HashSet
var productSet = new HashSet<Product>(comparer);
productSet.Add(new Product { SKU = "ABC-123", Name = "Widget" });
productSet.Add(new Product { SKU = "ABC-123", Name = "Widget (Duplicate)" });

// productSet.Count will be 1



📋 Practical Task

Exercise: Implementing a Case-Insensitive Employee Email Comparer

You are working with an Employee class that you cannot modify. Your goal is to ensure that a HashSet of employees treats two employees as the same if their email addresses match, regardless of whether the emails are uppercase or lowercase (e.g., "Alice@Company.com" and "alice@company.com" should be considered identical).

Requirements:

  • Create a class Employee with two properties: string Name and string Email.
  • Implement a class EmployeeEmailComparer that implements IEqualityComparer<Employee>.
  • In the Equals method, perform a case-insensitive comparison of the Email properties.
  • In the GetHashCode method, ensure that the hash is generated from the email in a way that is also case-insensitive (hint: normalize the string to lowercase before hashing).
  • In a Main method, create a HashSet<Employee> using your EmployeeEmailComparer. Add two Employee objects with the same email but different casing, and verify that the Count of the set is 1.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.