-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
242: Whiteboard Practice: Implementing a Custom Equality Comparer
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
Employeewith two properties:string Nameandstring Email. - Implement a class
EmployeeEmailComparerthat implementsIEqualityComparer<Employee>. - In the
Equalsmethod, perform a case-insensitive comparison of theEmailproperties. - In the
GetHashCodemethod, 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
Mainmethod, create aHashSet<Employee>using yourEmployeeEmailComparer. Add twoEmployeeobjects with the same email but different casing, and verify that theCountof the set is 1.
There are no comments for now.