C#において、自作したクラス(参照型)をコレクション(ListやHashSetなど)で扱う際、意図した通りに「等しい」と判定されないことがあります。これは、クラスのデフォルトの比較動作が「値(プロパティ)の一致」ではなく「参照(メモリアドレス)の一致」に基づいているためです。
HashSet<T>で重複を排除したり、List<T>.Containsメソッドで正しく検索を行ったりするためには、IEquatable<T>インターフェイスを実装し、適切な等価判定ロジックを定義する必要があります。
ここでは、倉庫管理システムにおける「在庫アイテム」を題材に、値に基づいて等しさを判定するクラスの実装方法を解説します。
IEquatable<T>の実装が必要な理由
通常、classで定義されたオブジェクトは、newで生成されるたびに異なるメモリ領域確保されるため、たとえプロパティの値がすべて同じであっても「別のオブジェクト」として扱われます。
IEquatable<T>を実装し、EqualsメソッドとGetHashCodeメソッドをオーバーライドすることで、開発者が定義したルール(例:商品コードとロット番号が同じなら等しい)に基づいてオブジェクトを同一視できるようになります。特にHashSet<T>やDictionary<TKey, TValue>のキーとしてオブジェクトを使用する場合、この実装は必須です。
実践的なコード例:在庫アイテムの重複排除
以下のコードは、商品コードとロット番号を持つInventoryItemクラスを定義し、その等価性を定義する例です。リストに同じデータを持つ別インスタンスを追加しても、HashSetによって重複が正しく排除される様子を確認できます。
using System;
using System.Collections.Generic;
namespace WarehouseManagement
{
class Program
{
static void Main()
{
// HashSetを使用して、重複するアイテムを自動的に排除するコレクションを作成
var uniqueInventory = new HashSet<InventoryItem>();
// 1. 商品A (ロット101) を追加
uniqueInventory.Add(new InventoryItem("PROD-A", 101));
// 2. 商品A (ロット101) を再度追加
// 参照は異なりますが、値が等しいためHashSetにより追加が無視されます。
bool isAdded = uniqueInventory.Add(new InventoryItem("PROD-A", 101));
Console.WriteLine($"重複データの追加結果: {isAdded}"); // False
// 3. 商品A (ロット102) を追加
// ロット番号が異なるため、別のアイテムとして追加されます。
uniqueInventory.Add(new InventoryItem("PROD-A", 102));
// 結果の出力
Console.WriteLine("\n--- 在庫リスト ---");
foreach (var item in uniqueInventory)
{
Console.WriteLine($"Code: {item.ProductCode}, Batch: {item.BatchNumber}");
}
}
}
/// <summary>
/// 在庫アイテムを表すクラス
/// IEquatable<T>を実装し、値による等価判定を提供します。
/// </summary>
public class InventoryItem : IEquatable<InventoryItem>
{
public string ProductCode { get; }
public int BatchNumber { get; }
public InventoryItem(string productCode, int batchNumber)
{
ProductCode = productCode;
BatchNumber = batchNumber;
}
// --- IEquatable<T> の実装 ---
/// <summary>
/// 別の InventoryItem オブジェクトと等しいか判定します。
/// </summary>
public bool Equals(InventoryItem other)
{
// 1. 相手が null なら等しくない
if (other is null) return false;
// 2. 参照が同じなら(自分自身なら)等しい
if (ReferenceEquals(this, other)) return true;
// 3. 全ての重要なプロパティの値が一致しているか確認
// 文字列比較には StringComparison.Ordinal などを指定するのが安全です
return ProductCode.Equals(other.ProductCode, StringComparison.Ordinal)
&& BatchNumber == other.BatchNumber;
}
// --- object のメソッドをオーバーライド ---
/// <summary>
/// オブジェクトとしての等価判定(非ジェネリック版)
/// </summary>
public override bool Equals(object obj)
{
// 型チェックを行い、InventoryItem型であればジェネリック版Equalsを呼び出す
return Equals(obj as InventoryItem);
}
/// <summary>
/// ハッシュコードの生成
/// HashSetやDictionaryで使用される重要なメソッドです。
/// </summary>
public override int GetHashCode()
{
// モダンなC#では HashCode.Combine を使用して安全にハッシュを生成します。
// これにより、ハッシュ衝突のリスクを低減できます。
return HashCode.Combine(ProductCode, BatchNumber);
}
// 必須ではありませんが、== および != 演算子もオーバーロードするのが一般的です。
public static bool operator ==(InventoryItem left, InventoryItem right)
{
if (left is null) return right is null;
return left.Equals(right);
}
public static bool operator !=(InventoryItem left, InventoryItem right)
{
return !(left == right);
}
}
}
実行結果
重複データの追加結果: False
--- 在庫リスト ---
Code: PROD-A, Batch: 101
Code: PROD-A, Batch: 102
技術的なポイントと注意点
1. 3点セットでの実装
等価判定をカスタマイズする場合、原則として以下の3つをセットで実装します。
IEquatable<T>.Equals(T other)メソッドobject.Equals(object obj)メソッドのオーバーライドobject.GetHashCode()メソッドのオーバーライド
特に GetHashCode の実装を忘れると、HashSet や Dictionary が正しく動作しません(ハッシュコードが異なると、そもそも Equals による比較が行われないためです)。
2. HashCode.Combineの利用
古いコード例では XOR演算子(^)を使ってハッシュコードを作成する例が見られますが、これはハッシュ衝突(異なる値なのに同じハッシュ値になること)が発生しやすいため推奨されません。 .NET Core 2.1以降(および.NET Standard 2.1以降)では、HashCode.Combineメソッドを使用することで、高速かつ分散の良いハッシュコードを簡単に生成できます。
3. C# 9.0以降の record 型
もしクラスが「データの保持」のみを目的としており、不変(Immutable)でよいのであれば、classの代わりにrecord型を使用することを検討してください。record型は、上記のような等価判定ロジック(IEquatableの実装、Equals、GetHashCode、==演算子)をコンパイラが自動的に生成してくれるため、コード量を大幅に削減できます。
まとめ
IEquatable<T>を適切に実装することで、自作クラスを数値や文字列と同じように「値」として扱うことができるようになります。コレクション内での検索や重複排除を行う際には不可欠なテクニックですので、EqualsとGetHashCodeの関係性を理解し、正しく実装してください。
