通常、クラスの等価判定(2つのオブジェクトが同じかどうか)は、そのクラス自身にIEquatable<T>を実装して定義します。しかし、以下のようなケースではそれが適さない場合があります。
- ソースコードを変更できないクラス(外部ライブラリなど)の比較ロジックを変えたい場合。
- 状況に応じて異なる比較ルールを適用したい場合(例:IDだけで比較したい場合と、全プロパティで比較したい場合)。
このような場面では、IEqualityComparer<T>インターフェイスを実装した「比較専用のクラス」を作成し、それをHashSetやDictionary、LINQのDistinctなどに渡す手法が有効です。
今回は、配送システムにおける「ダンボール箱(Carton)」のサイズ比較を題材に、クラスを変更せずに外部から重複判定を行う方法を解説します。
IEqualityComparer<T>の概要
このインターフェイスを実装するクラスは、対象となる型Tの比較ルールを定義します。実装が必要なメソッドは以下の2つです。
bool Equals(T x, T y): 2つのオブジェクトが等しいかどうかを判定します。int GetHashCode(T obj): オブジェクトのハッシュ値を返します(Equalsがtrueを返すオブジェクト同士は、必ず同じハッシュ値を返す必要があります)。
実践的なコード例:サイズの同じ箱を同一視する
以下のコードでは、Cartonクラス自体には特別な比較ロジックを持たせず、別途定義したCartonSizeComparerクラスによって、幅と高さが同じ箱を「重複」としてHashSetから排除しています。
using System;
using System.Collections.Generic;
namespace LogisticsSystem
{
class Program
{
static void Main()
{
// 比較ルール(Comparer)をインスタンス化
var sizeComparer = new CartonSizeComparer();
// HashSetのコンストラクタにComparerを渡すのがポイント
// これにより、このHashSetは指定されたルールに従って重複排除を行います。
var warehouse = new HashSet<Carton>(sizeComparer);
// 1. サイズ 10x20 の箱を追加
warehouse.Add(new Carton(10, 20));
// 2. サイズ 10x20 の箱(別インスタンス)を追加
// Comparerにより「等しい」とみなされ、追加されません。
bool isAdded = warehouse.Add(new Carton(10, 20));
Console.WriteLine($"重複データの追加結果: {isAdded}"); // False
// 3. サイズ 15x20 の箱を追加
warehouse.Add(new Carton(15, 20));
// 結果の出力
Console.WriteLine("\n--- 倉庫内の箱リスト ---");
foreach (var item in warehouse)
{
Console.WriteLine($"Height: {item.Height}, Width: {item.Width}");
}
}
}
// データクラス(POCO)
// このクラス自体には比較ロジックを持たせません。
public class Carton
{
public int Height { get; }
public int Width { get; }
public Carton(int height, int width)
{
Height = height;
Width = width;
}
}
// 比較ロジックを独立させたクラス
// IEqualityComparer<Carton> を実装します。
public class CartonSizeComparer : IEqualityComparer<Carton>
{
// 2つのオブジェクトが等しいかどうかのロジック
public bool Equals(Carton x, Carton y)
{
// 参照が同じなら等しい
if (ReferenceEquals(x, y)) return true;
// どちらかがnullなら等しくない
if (x is null || y is null) return false;
// サイズ(高さと幅)が一致していれば等しいとみなす
return x.Height == y.Height && x.Width == y.Width;
}
// ハッシュコードの生成ロジック
public int GetHashCode(Carton obj)
{
if (obj is null) return 0;
// C#のモダンなハッシュ生成手法
// Equalsで比較に使用したプロパティのみを使ってハッシュを作る必要があります。
return HashCode.Combine(obj.Height, obj.Width);
}
}
}
実行結果
重複データの追加結果: False
--- 倉庫内の箱リスト ---
Height: 10, Width: 20
Height: 15, Width: 20
技術的なポイントと実装のコツ
1. どこで使うのか
作成したComparerは、主に以下の場所で使用します。
- コレクションのコンストラクタ:
HashSet<T>,Dictionary<TKey, TValue> - LINQメソッドの引数:
Distinct,Intersect,Union,Except,Containsなど
// LINQのDistinctで使用する例
var distinctCartons = cartonsList.Distinct(new CartonSizeComparer());
2. Nullチェックの重要性
Equals(T x, T y) メソッドの実装では、引数 x や y が null である可能性を考慮する必要があります。これを怠ると NullReferenceException の原因となります。
3. GetHashCodeの実装義務
IEqualityComparer<T> において Equals と GetHashCode は密接に関係しています。 「Equalsがtrueを返すなら、GetHashCodeも同じ値を返さなければならない」 という絶対的なルールがあります。これが守られていない場合、HashSet や Dictionary は正しく動作しません(検索や重複排除に失敗します)。
4. シングルトンパターン
Comparerクラスは状態を持たない(ステートレス)場合が多いため、毎回 new するのではなく、静的なシングルトンプロパティとして公開するパターンがよく使われます。
public class CartonSizeComparer : IEqualityComparer<Carton>
{
// シングルトンインスタンス
public static CartonSizeComparer Default { get; } = new CartonSizeComparer();
// ... 実装は同じ ...
}
// 利用時
var set = new HashSet<Carton>(CartonSizeComparer.Default);
まとめ
IEqualityComparer<T> を利用することで、既存のクラス設計に手を加えることなく、柔軟な重複判定や検索ロジックを注入できます。「データそのもの」と「比較方法」を分離する設計手法として、コレクション操作の場面で積極的に活用してください。
