[C#] Implementing Equality Logic Outside Classes with IEqualityComparer

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

  1. Overview of IEqualityComparer<T>
  2. Practical Code Example: Treating Same-Sized Boxes as Identical
  3. Execution Result
  4. Technical Points and Implementation Tips
    1. Where to Use It
    2. Importance of Null Checks
    3. Obligation to Implement GetHashCode
    4. Singleton Pattern
  5. 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 which Equals returns 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.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次