Usually, equality determination for a class (whether two objects are the same) is defined by implementing IEquatable<T> within the class itself. However, this approach may not be suitable in the following cases:
- When you want to change the comparison logic of a class whose source code you cannot modify (e.g., external libraries).
- When you want to apply different comparison rules depending on the situation (e.g., comparing only by ID in one case, and by all properties in another).
In such situations, creating a “comparison-specific class” that implements the IEqualityComparer<T> interface and passing it to HashSet, Dictionary, or LINQ’s Distinct is an effective technique.
In this article, I will explain how to determine duplicates externally without modifying the class, using the size comparison of “Cartons” (cardboard boxes) in a delivery system as an example.
Table of Contents
- Overview of IEqualityComparer<T>
- Practical Code Example: Treating Same-Sized Boxes as Identical
- Execution Result
- Technical Points and Implementation Tips
- Where to Use It
- Importance of Null Checks
- Obligation to Implement GetHashCode
- Singleton Pattern
- Summary
Overview of IEqualityComparer<T>
A class implementing this interface defines the comparison rules for the target type T. The following two methods must be implemented:
bool Equals(T x, T y): Determines whether two objects are equal.int GetHashCode(T obj): Returns the hash value of the object. (Objects for whichEqualsreturns true must return the same hash value).
Practical Code Example: Treating Same-Sized Boxes as Identical
In the following code, the Carton class itself does not have any special comparison logic. Instead, a separately defined CartonSizeComparer class is used to exclude boxes with the same width and height as “duplicates” from a HashSet.
using System;
using System.Collections.Generic;
namespace LogisticsSystem
{
class Program
{
static void Main()
{
// Instantiate the comparison rule (Comparer)
var sizeComparer = new CartonSizeComparer();
// The key is to pass the Comparer to the HashSet constructor.
// This ensures the HashSet performs deduplication according to the specified rule.
var warehouse = new HashSet<Carton>(sizeComparer);
// 1. Add a 10x20 box
warehouse.Add(new Carton(10, 20));
// 2. Add another 10x20 box (different instance)
// The Comparer deems this "equal", so it is not added.
bool isAdded = warehouse.Add(new Carton(10, 20));
Console.WriteLine($"Result of adding duplicate data: {isAdded}"); // False
// 3. Add a 15x20 box
warehouse.Add(new Carton(15, 20));
// Output results
Console.WriteLine("\n--- Box List in Warehouse ---");
foreach (var item in warehouse)
{
Console.WriteLine($"Height: {item.Height}, Width: {item.Width}");
}
}
}
// Data Class (POCO)
// This class itself does not hold any comparison logic.
public class Carton
{
public int Height { get; }
public int Width { get; }
public Carton(int height, int width)
{
Height = height;
Width = width;
}
}
// Class with isolated comparison logic
// Implements IEqualityComparer<Carton>
public class CartonSizeComparer : IEqualityComparer<Carton>
{
// Logic to determine if two objects are equal
public bool Equals(Carton x, Carton y)
{
// If references are the same, they are equal
if (ReferenceEquals(x, y)) return true;
// If either is null, they are not equal
if (x is null || y is null) return false;
// Consider them equal if sizes (Height and Width) match
return x.Height == y.Height && x.Width == y.Width;
}
// Logic to generate hash code
public int GetHashCode(Carton obj)
{
if (obj is null) return 0;
// Modern C# hash generation method
// You must create the hash using only the properties used in Equals.
return HashCode.Combine(obj.Height, obj.Width);
}
}
}
Execution Result
Result of adding duplicate data: False
--- Box List in Warehouse ---
Height: 10, Width: 20
Height: 15, Width: 20
Technical Points and Implementation Tips
1. Where to Use It
The created Comparer is mainly used in the following places:
- Collection Constructors:
HashSet<T>,Dictionary<TKey, TValue> - LINQ Method Arguments:
Distinct,Intersect,Union,Except,Contains, etc.
// Example usage in LINQ Distinct
var distinctCartons = cartonsList.Distinct(new CartonSizeComparer());
2. Importance of Null Checks
In the implementation of Equals(T x, T y), you must consider the possibility that arguments x or y are null. Neglecting this will cause a NullReferenceException.
3. Obligation to Implement GetHashCode
In IEqualityComparer<T>, Equals and GetHashCode are closely related. There is an absolute rule: “If Equals returns true, GetHashCode must return the same value.”
If this rule is violated, HashSet and Dictionary will not function correctly (lookups and deduplication will fail).
4. Singleton Pattern
Since Comparer classes are often stateless, it is a common pattern to expose a static singleton property instead of new-ing it every time.
public class CartonSizeComparer : IEqualityComparer<Carton>
{
// Singleton Instance
public static CartonSizeComparer Default { get; } = new CartonSizeComparer();
// ... Implementation remains the same ...
}
// Usage
var set = new HashSet<Carton>(CartonSizeComparer.Default);
Summary
By using IEqualityComparer<T>, you can inject flexible deduplication or search logic without modifying existing class designs. Actively use this as a design pattern to separate “the data itself” from “how it is compared” when performing collection operations.
