- はじめに:「比較用EqualityComparer」は“何をもって同じとみなすか”をコードにする道具
- 基本:IEqualityComparer<T> とは何か
- 例1:ユーザーIDで比較する EqualityComparer
- EqualityComparer を LINQ で使う
- 例2:複数項目で比較する EqualityComparer
- Dictionary / HashSet での EqualityComparer 利用
- 例3:文字列の大文字小文字を無視する Comparer
- EqualityComparer をユーティリティとしてまとめる
- まとめ:「比較用EqualityComparer」は“業務ルールをそのまま比較ロジックにする”ための鍵
はじめに:「比較用EqualityComparer」は“何をもって同じとみなすか”をコードにする道具
LINQ やコレクションを使っていると、こういう場面がよく出てきます。
「ユーザーIDが同じなら同じユーザーとして扱いたい」
「名前と生年月日が同じなら同一人物とみなしたい」
「大文字小文字を無視して文字列を比較したい」
でも、C# の標準の「等価比較」は、クラスだと「参照が同じかどうか」、文字列だと「大文字小文字を区別して一致するかどうか」です。
ここを自分のルールに変えたいときに使うのが 比較用EqualityComparer(IEqualityComparer<T>) です。
「何をもって同じとみなすか」をクラスとして切り出しておくと、
Distinct、GroupBy、Except、Dictionary、HashSet などで、
“業務的な同一性”をそのまま使えるようになります。
基本:IEqualityComparer<T> とは何か
Equals と GetHashCode をセットで定義する
IEqualityComparer<T> は、ざっくり言うと「T 型同士を比較するルール」を表すインターフェースです。
public interface IEqualityComparer<T>
{
bool Equals(T? x, T? y);
int GetHashCode(T obj);
}
C#Equals で「同じかどうか」を決め、
GetHashCode で「同じものは同じハッシュ値になるように」整数を返します。
重要なのは、この 2 つが矛盾しないことです。
Equals が true を返す 2 つの値は、必ず同じハッシュコードを返さなければいけません。
例1:ユーザーIDで比較する EqualityComparer
モデルクラスを用意する
まず、よくある User クラスを用意します。
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
C#標準の比較では、「同じ Id でも、別インスタンスなら別物」です。
これを「Id が同じなら同じユーザー」とみなしたいとします。
IEqualityComparer<User> を実装する
public class UserIdEqualityComparer : IEqualityComparer<User>
{
public bool Equals(User? x, User? y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.Id == y.Id;
}
public int GetHashCode(User obj)
{
return obj.Id.GetHashCode();
}
}
C#Equals では null や同一参照を丁寧に扱い、
最後に「Id が同じなら true」としています。
GetHashCode では Id のハッシュコードをそのまま返しています。
ここでの重要ポイントは、「Equals の条件と GetHashCode の計算が“同じキー(ここでは Id)”に基づいていること」です。
これがズレると、Dictionary や Distinct の動きがおかしくなります。
EqualityComparer を LINQ で使う
Distinct に渡して「業務的な重複排除」をする
ユーザー一覧に、同じ Id のユーザーが重複しているとします。
var users = new[]
{
new User { Id = 1, Name = "Alice" },
new User { Id = 1, Name = "Alice (duplicate)" },
new User { Id = 2, Name = "Bob" },
};
C#標準の Distinct は「参照が同じかどうか」で見てしまうので、全部別物として扱われます。
ここで UserIdEqualityComparer を渡すと、「Id が同じものは 1 つにまとめる」ことができます。
var distinct = users
.Distinct(new UserIdEqualityComparer())
.ToList();
C#この distinct には、Id=1 のユーザーが 1 件、Id=2 が 1 件だけ残ります。
ここでの重要ポイントは、「Distinct、Except、Intersect、GroupBy など、多くの LINQ 演算子は IEqualityComparer<T> を受け取れる」ということです。
“何をもって同じとみなすか”を差し替えられます。
GroupBy に渡して「業務キーでグルーピング」する
例えば、「Id ごとにユーザーをまとめたい」場合。
var groups = users
.GroupBy(x => x, new UserIdEqualityComparer());
C#キーに User 自体を渡しつつ、Comparer で「Id が同じなら同じグループ」とみなすことができます。
もちろん、普通に GroupBy(x => x.Id) と書いてもいいですが、
複数プロパティで比較したいときなどに EqualityComparer が活きてきます。
例2:複数項目で比較する EqualityComparer
名前と生年月日が同じなら同一人物とみなす
public class Person
{
public string Name { get; set; } = "";
public DateTime BirthDate { get; set; }
}
C#これを「Name と BirthDate が同じなら同じ人」とみなす Comparer を作ります。
public class PersonIdentityComparer : IEqualityComparer<Person>
{
public bool Equals(Person? x, Person? y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.Name == y.Name
&& x.BirthDate == y.BirthDate;
}
public int GetHashCode(Person obj)
{
return HashCode.Combine(obj.Name, obj.BirthDate);
}
}
C#GetHashCode では HashCode.Combine を使うと、複数項目をきれいにまとめられます。
ここでの重要ポイントは、「Equals で使う全ての項目を、GetHashCode にも必ず反映する」ことです。
一部だけをハッシュに使うと、衝突が増えたり、動きが直感とズレたりします。
Dictionary / HashSet での EqualityComparer 利用
Dictionary のキーとして「業務キー」を使う
Dictionary<TKey, TValue> は、キーの比較に IEqualityComparer<TKey> を使います。
コンストラクタで渡すことができます。
var dict = new Dictionary<User, string>(
new UserIdEqualityComparer());
dict[new User { Id = 1, Name = "A" }] = "First";
dict[new User { Id = 1, Name = "B" }] = "Second";
C#ここでは「Id が同じなら同じキー」とみなされるので、
2 行目の代入は 1 行目を上書きします。
HashSet も同様です。
var set = new HashSet<User>(new UserIdEqualityComparer());
set.Add(new User { Id = 1, Name = "A" });
set.Add(new User { Id = 1, Name = "B" }); // 追加されない(同じとみなされる)
C#ここでの重要ポイントは、「Dictionary や HashSet に“業務的な同一性”を持ち込むには、EqualityComparer を渡すのが王道」ということです。
例3:文字列の大文字小文字を無視する Comparer
既製品を知っておく:StringComparer
文字列の比較でよくあるのが、「大文字小文字を無視したい」という要件です。
これは自分で IEqualityComparer<string> を書かなくても、StringComparer という既製品があります。
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
set.Add("ABC");
set.Add("abc"); // 追加されない(同じとみなされる)
C#LINQ でも同様に使えます。
var words = new[] { "Apple", "apple", "Banana" };
var distinct = words
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
C#ここでの重要ポイントは、「よくあるパターン(文字列の比較など)は、まず既製の Comparer がないか探す」ということです。
自分で書くのは、それでも足りないときで十分です。
EqualityComparer をユーティリティとしてまとめる
静的プロパティで「共通の比較ルール」を共有する
毎回 new UserIdEqualityComparer() と書くのは少し面倒なので、
クラス側に静的プロパティとして持たせるのもよくあるパターンです。
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public static IEqualityComparer<User> IdComparer { get; }
= new UserIdEqualityComparer();
}
C#使う側はこう書けます。
var distinct = users.Distinct(User.IdComparer).ToList();
var set = new HashSet<User>(User.IdComparer);
C#ここでの重要ポイントは、「Comparer を“どこに置くか”も設計の一部」ということです。
ドメインに密着した比較ルールなら、モデルクラスの近くに置いておくと見通しが良くなります。
まとめ:「比較用EqualityComparer」は“業務ルールをそのまま比較ロジックにする”ための鍵
比較用EqualityComparer の本質は、
「何をもって同じとみなすか」という業務ルールを、IEqualityComparer<T> としてコード化し、
LINQ や Dictionary / HashSet に差し込めるようにする
ことです。
押さえておきたいポイントは次の通りです。
Equals と GetHashCode をセットで実装し、同じキーに基づかせる
Distinct / GroupBy / Except / Intersect など、多くの LINQ 演算子が Comparer を受け取れる
Dictionary / HashSet に Comparer を渡すと、「業務キーでの重複判定」ができる
複数項目で比較するときは HashCode.Combine を使うと書きやすい
よくあるパターン(文字列の大文字小文字無視など)は StringComparer など既製品も活用する
ここまで理解できていれば、
「なんとなく標準の比較に任せる」段階から抜け出して、
“業務の同一性”をそのままコードに落とし込める C# エンジニアに近づけます。
