C# HashSetの使い方完全ガイド|重複削除・高速検索・Listとの違いを初心者向けに解説

はじめに

C#で「重複しないデータを扱いたい」「大量のデータから高速に存在チェックしたい」ときに便利なのがHashSet<T>です。

たとえば、次のような場面で役立ちます。

C#
var userIds = new HashSet<int>();

userIds.Add(1001);
userIds.Add(1002);
userIds.Add(1001);

Console.WriteLine(userIds.Count); // 2

1001を2回追加していますが、HashSetでは同じ値は1つだけ保持されます。

C#ではList<T>を使う機会が多いですが、用途によってはHashSet<T>を使ったほうがコードがシンプルになり、処理速度も速くなります。この記事では、C#のHashSetの基本的な使い方から、重複削除、高速検索、Listとの違い、集合演算、独自クラスでの重複判定まで初心者向けに解説します。

1. C#のHashSetとは?重複しないデータを扱うコレクション

1-1. HashSetの基本的な役割

HashSet<T>は、重複しない要素の集合を扱うためのコレクションです。

Tには、intstring、独自クラスなど、さまざまな型を指定できます。

C#
HashSet<string> names = new HashSet<string>();

names.Add("Alice");
names.Add("Bob");
names.Add("Alice");

Console.WriteLine(names.Count); // 2

List<T>では同じ値を何度でも追加できますが、HashSet<T>では同じ値を追加しても1つだけしか保持されません。

つまり、HashSetは次のような目的でよく使われます。

C#
// 重複しないIDを管理する
HashSet<int> ids = new HashSet<int>();

// 登録済みユーザー名を管理する
HashSet<string> userNames = new HashSet<string>();

// 既に処理したデータを記録する
HashSet<string> processedItems = new HashSet<string>();

「同じものを2回入れたくない」という場面では、HashSet<T>が非常に便利です。

1-2. HashSetが重複を自動で排除できる仕組み

HashSetは、内部的にハッシュ値を使って要素を管理しています。

ハッシュ値とは、値から計算される番号のようなものです。HashSetはこのハッシュ値を利用して、同じ値がすでに存在するかどうかを効率よく判定します。

たとえば、次のコードでは"apple"を2回追加しています。

C#
var fruits = new HashSet<string>();

bool result1 = fruits.Add("apple");
bool result2 = fruits.Add("banana");
bool result3 = fruits.Add("apple");

Console.WriteLine(result1); // True
Console.WriteLine(result2); // True
Console.WriteLine(result3); // False
Console.WriteLine(fruits.Count); // 2

Addメソッドは、追加できた場合はtrue、すでに同じ値が存在して追加されなかった場合はfalseを返します。

そのため、重複チェックと追加を同時に行いたい場合にも便利です。

C#
if (fruits.Add("orange"))
{
Console.WriteLine("追加しました");
}
else
{
Console.WriteLine("すでに存在します");
}

1-3. HashSetが向いているケース・向いていないケース

HashSetが向いているのは、次のようなケースです。

ケースHashSetが向いている理由
重複を削除したい同じ値を自動で1つにまとめられる
存在チェックを高速に行いたいContainsが高速
すでに処理済みか判定したい処理済みデータの管理に便利
2つのデータ群の共通・差分を求めたい集合演算が使える

一方で、次のようなケースにはあまり向いていません。

ケース理由
要素の順番を厳密に管理したい順序は基本的に保証されない
インデックスでアクセスしたいhashSet[0]のようなアクセスはできない
同じ値を複数保持したい重複を許可しない
小さなデータを単純に順番どおり扱いたいList<T>のほうがわかりやすい

HashSetは「重複なし」「高速検索」「集合演算」に強いコレクションと覚えておくとよいでしょう。

2. HashSetの基本的な使い方

2-1. HashSetの宣言と初期化

HashSet<T>を使うには、通常は次の名前空間を使用します。

C#
using System.Collections.Generic;

基本的な宣言方法は次のとおりです。

C#
HashSet<int> numbers = new HashSet<int>();

C#のバージョンによっては、次のように簡潔に書くこともできます。

C#
var numbers = new HashSet<int>();

初期値を入れて作成することもできます。

C#
var fruits = new HashSet<string>
{
"apple",
"banana",
"orange"
};

重複した値を初期値に含めた場合でも、自動的に1つにまとめられます。

C#
var numbers = new HashSet<int>
{
1,
2,
2,
3,
3,
3
};

Console.WriteLine(numbers.Count); // 3

2-2. Addで要素を追加する

HashSetに要素を追加するにはAddメソッドを使います。

C#
var set = new HashSet<string>();

set.Add("C#");
set.Add("Java");
set.Add("Python");

同じ要素を追加しても、重複して保存されません。

C#
set.Add("C#");
set.Add("C#");

Console.WriteLine(set.Count); // 1

Addの戻り値を使うと、追加に成功したかどうかを判定できます。

C#
var languages = new HashSet<string>();

if (languages.Add("C#"))
{
Console.WriteLine("C#を追加しました");
}
else
{
Console.WriteLine("C#はすでに存在します");
}

重複登録を防ぎたい場合は、Containsで確認してからAddするよりも、Addの戻り値を使うほうが簡潔です。

C#
if (!languages.Add("C#"))
{
Console.WriteLine("重複しています");
}

2-3. Removeで要素を削除する

要素を削除するにはRemoveメソッドを使います。

C#
var numbers = new HashSet<int> { 1, 2, 3, 4, 5 };

numbers.Remove(3);

Console.WriteLine(numbers.Contains(3)); // False

Removeも戻り値としてboolを返します。削除できた場合はtrue、対象の要素が存在しなかった場合はfalseです。

C#
bool removed = numbers.Remove(10);

Console.WriteLine(removed); // False

削除できたかどうかで処理を分けることもできます。

C#
if (numbers.Remove(2))
{
Console.WriteLine("削除しました");
}
else
{
Console.WriteLine("対象が見つかりませんでした");
}

2-4. Containsで要素の存在を確認する

HashSetで特によく使うのがContainsメソッドです。

C#
var ids = new HashSet<int> { 101, 102, 103 };

if (ids.Contains(102))
{
Console.WriteLine("ID 102は存在します");
}

Containsは、指定した要素がHashSet内に存在するかを判定します。

C#
Console.WriteLine(ids.Contains(101)); // True
Console.WriteLine(ids.Contains(999)); // False

HashSetContainsは、大量データに対する検索で特に強力です。List<T>Containsは先頭から順番に探しますが、HashSet<T>はハッシュ値を使って効率よく探します。

2-5. Countで要素数を取得する

HashSetに含まれる要素数はCountプロパティで取得できます。

C#
var colors = new HashSet<string>
{
"red",
"blue",
"green",
"red"
};

Console.WriteLine(colors.Count); // 3

重複した"red"は1つとして扱われるため、要素数は3になります。

空かどうかを確認したい場合は、Countを使えます。

C#
if (colors.Count == 0)
{
Console.WriteLine("要素はありません");
}
else
{
Console.WriteLine("要素があります");
}

2-6. foreachでHashSetの中身を取り出す

HashSetの中身はforeachで取り出せます。

C#
var fruits = new HashSet<string>
{
"apple",
"banana",
"orange"
};

foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}

ただし、HashSetは順序を管理するためのコレクションではありません。

C#
var numbers = new HashSet<int> { 3, 1, 2 };

foreach (var number in numbers)
{
Console.WriteLine(number);
}

出力順が必ず3, 1, 2になるとは考えないほうが安全です。順序が重要な場合は、List<T>OrderByを使って並べ替える必要があります。

C#
foreach (var number in numbers.OrderBy(x => x))
{
Console.WriteLine(number);
}

このコードでOrderByを使う場合は、次の名前空間も必要です。

C#
using System.Linq;

3. HashSetで重複を削除する方法

3-1. ListからHashSetに変換して重複削除する

List<T>の重複を削除したい場合、HashSet<T>に変換する方法が簡単です。

C#
var list = new List<int> { 1, 2, 2, 3, 3, 4 };

var set = new HashSet<int>(list);

foreach (var number in set)
{
Console.WriteLine(number);
}

HashSetに変換した時点で、重複した値は自動的に取り除かれます。

文字列でも同じように使えます。

C#
var names = new List<string>
{
"Alice",
"Bob",
"Alice",
"Charlie",
"Bob"
};

var uniqueNames = new HashSet<string>(names);

Console.WriteLine(uniqueNames.Count); // 3

3-2. HashSetからListに戻す方法

重複を削除したあと、再びList<T>として扱いたい場合は、ToListを使います。

C#
using System.Linq;

var list = new List<int> { 1, 2, 2, 3, 3, 4 };

var set = new HashSet<int>(list);
var uniqueList = set.ToList();

foreach (var number in uniqueList)
{
Console.WriteLine(number);
}

または、List<T>のコンストラクターにHashSet<T>を渡すこともできます。

C#
var uniqueList = new List<int>(set);

ただし、HashSetに変換すると順序が保証されない点に注意してください。

元の順番をできるだけ保ったまま重複削除したい場合は、LINQのDistinctを使うほうが適していることがあります。

C#
var uniqueList = list.Distinct().ToList();

3-3. 文字列・数値・オブジェクトの重複削除例

数値の重複削除は次のように書けます。

C#
var numbers = new List<int> { 10, 20, 10, 30, 20 };

var uniqueNumbers = new HashSet<int>(numbers);

foreach (var number in uniqueNumbers)
{
Console.WriteLine(number);
}

文字列の重複削除も同じです。

C#
var emails = new List<string>
{
"a@example.com",
"b@example.com",
"a@example.com"
};

var uniqueEmails = new HashSet<string>(emails);

foreach (var email in uniqueEmails)
{
Console.WriteLine(email);
}

独自クラスの場合は注意が必要です。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}

このままHashSet<User>に入れると、同じIdや同じNameを持っていても別オブジェクトとして扱われる場合があります。

C#
var users = new HashSet<User>();

users.Add(new User { Id = 1, Name = "Alice" });
users.Add(new User { Id = 1, Name = "Alice" });

Console.WriteLine(users.Count); // 2になる可能性がある

独自クラスで「同じユーザー」と判定したい場合は、EqualsGetHashCodeを実装するか、IEqualityComparer<T>を使って比較ルールを指定します。

3-4. LINQのDistinctとの違い

C#で重複削除をするときは、HashSetのほかにLINQのDistinctもよく使われます。

C#
var numbers = new List<int> { 1, 2, 2, 3, 3, 4 };

var uniqueNumbers = numbers.Distinct().ToList();

HashSetDistinctの違いは、主に用途です。

方法向いている用途
HashSet重複を防ぎながらデータを追加したい
Distinct既存のデータから一度だけ重複削除したい

たとえば、データを追加しながら重複チェックしたい場合はHashSetが便利です。

C#
var set = new HashSet<string>();

foreach (var name in names)
{
if (set.Add(name))
{
Console.WriteLine($"{name}を初めて追加しました");
}
}

一方、すでにあるList<T>から重複を取り除くだけならDistinctが読みやすいです。

C#
var uniqueNames = names.Distinct().ToList();

つまり、継続的に重複を防ぐならHashSet、一時的な重複削除ならDistinctと考えるとわかりやすいです。

4. HashSetが高速検索に強い理由

4-1. HashSetの検索速度はなぜ速いのか

HashSetが高速検索に強い理由は、内部でハッシュテーブルの仕組みを使っているためです。

List<T>Containsを使う場合、基本的には先頭から順番に値を探します。

C#
var list = new List<int> { 1, 2, 3, 4, 5 };

Console.WriteLine(list.Contains(5)); // 先頭から順番に探す

データが少ない場合は問題ありませんが、数万件、数十万件、数百万件になると検索に時間がかかることがあります。

一方、HashSet<T>はハッシュ値を使って、目的の値がある場所を効率よく探します。

C#
var set = new HashSet<int> { 1, 2, 3, 4, 5 };

Console.WriteLine(set.Contains(5)); // 高速に探しやすい

そのため、何度も存在チェックを行う処理ではHashSetが有効です。

4-2. Containsの処理速度をListと比較

List<T>HashSet<T>Containsを比較すると、特に大量データで差が出ます。

C#
var list = Enumerable.Range(1, 1_000_000).ToList();
var set = new HashSet<int>(list);

Console.WriteLine(list.Contains(999_999));
Console.WriteLine(set.Contains(999_999));

List<T>の場合、目的の値が後ろにあるほど探すのに時間がかかります。

C#
list.Contains(999_999);

一方、HashSet<T>はデータ数が増えても、比較的安定して高速に検索できます。

C#
set.Contains(999_999);

何度も検索する処理であれば、最初にList<T>からHashSet<T>へ変換しておくと効率的です。

C#
var targetIds = new HashSet<int>(list);

foreach (var id in checkIds)
{
if (targetIds.Contains(id))
{
Console.WriteLine($"存在します: {id}");
}
}

4-3. 大量データでHashSetを使うメリット

大量データでHashSetを使うメリットは、存在チェックや重複チェックが簡単かつ高速になることです。

たとえば、次のように処理済みIDを管理できます。

C#
var processedIds = new HashSet<int>();

foreach (var id in inputIds)
{
if (!processedIds.Add(id))
{
Console.WriteLine($"重複IDです: {id}");
continue;
}

Console.WriteLine($"処理します: {id}");
}

このコードでは、Addの戻り値を使って「初めて出てきたIDかどうか」を判定しています。

大量のログやCSVデータを処理する場合にも、HashSetはよく使われます。

C#
var seenEmails = new HashSet<string>();

foreach (var email in emails)
{
if (seenEmails.Contains(email))
{
Console.WriteLine($"重複メールアドレス: {email}");
}
else
{
seenEmails.Add(email);
}
}

さらに簡潔に書くなら、次のようにできます。

C#
foreach (var email in emails)
{
if (!seenEmails.Add(email))
{
Console.WriteLine($"重複メールアドレス: {email}");
}
}

4-4. HashSetを使うとパフォーマンスが悪くなるケース

HashSetは便利ですが、常に最適とは限りません。

次のようなケースでは、List<T>のほうが適している場合があります。

C#
var numbers = new List<int> { 1, 2, 3 };

データ件数が非常に少なく、検索回数も少ない場合は、HashSetにするメリットはあまり大きくありません。

また、HashSetは内部的にハッシュテーブルを使うため、List<T>よりメモリを多く使うことがあります。

さらに、独自クラスでGetHashCodeの実装が悪いと、検索性能が落ちる可能性があります。

C#
public override int GetHashCode()
{
return 1; // 悪い例:すべて同じハッシュ値になる
}

このような実装では、ハッシュ値による分類がうまく働かず、HashSetの性能を活かせません。

HashSetを使うべきかどうかは、次の基準で考えるとよいでしょう。

判断基準おすすめ
重複を許したくないHashSet
存在チェックが多いHashSet
順序が重要List
インデックスアクセスしたいList
データが少なく単純Listでも十分

5. HashSetとListの違い

5-1. 重複を許すかどうかの違い

HashSet<T>List<T>の大きな違いは、重複を許すかどうかです。

List<T>は重複を許します。

C#
var list = new List<int>();

list.Add(1);
list.Add(1);
list.Add(1);

Console.WriteLine(list.Count); // 3

一方、HashSet<T>は重複を許しません。

C#
var set = new HashSet<int>();

set.Add(1);
set.Add(1);
set.Add(1);

Console.WriteLine(set.Count); // 1

同じ値を複数回保持したい場合はList<T>、重複させたくない場合はHashSet<T>を使います。

5-2. 順序を保持するかどうかの違い

List<T>は追加した順番で要素を管理します。

C#
var list = new List<string>();

list.Add("A");
list.Add("B");
list.Add("C");

Console.WriteLine(list[0]); // A
Console.WriteLine(list[1]); // B
Console.WriteLine(list[2]); // C

HashSet<T>は、順序を前提に使うコレクションではありません。

C#
var set = new HashSet<string>();

set.Add("A");
set.Add("B");
set.Add("C");

foreach (var item in set)
{
Console.WriteLine(item);
}

実行結果が追加順になるように見えることもありますが、その順序に依存するコードは避けるべきです。

順序が必要ならList<T>を使いましょう。

5-3. 検索速度の違い

List<T>Containsは、基本的に要素を順番に確認します。

C#
var list = new List<int> { 1, 2, 3, 4, 5 };

bool exists = list.Contains(5);

要素数が多い場合、目的の値が見つかるまで時間がかかることがあります。

HashSet<T>Containsは、ハッシュ値を利用して高速に検索できます。

C#
var set = new HashSet<int> { 1, 2, 3, 4, 5 };

bool exists = set.Contains(5);

大量データに対して何度も存在チェックを行うなら、HashSet<T>が有利です。

5-4. インデックスアクセスの可否

List<T>はインデックスでアクセスできます。

C#
var list = new List<string> { "A", "B", "C" };

Console.WriteLine(list[0]); // A

一方、HashSet<T>はインデックスでアクセスできません。

C#
var set = new HashSet<string> { "A", "B", "C" };

// Console.WriteLine(set[0]); // コンパイルエラー

特定の位置にある要素を取り出したい場合は、HashSetではなくListを使うのが自然です。

どうしてもHashSetから一部の要素を取り出したい場合は、Firstなどを使う方法もあります。

C#
using System.Linq;

var first = set.First();

ただし、この場合も「最初の要素」が追加順の先頭であるとは考えないようにしましょう。

5-5. HashSetとListの使い分け早見表

HashSet<T>List<T>の使い分けは、次の表で整理できます。

比較項目HashSet<T>List<T>
重複許さない許す
順序基本的に保証されない保持する
検索高速要素数が多いと遅くなりやすい
インデックスアクセスできないできる
集合演算得意標準ではやや手間
主な用途重複排除、存在チェック順序付きデータ管理

使い分けの目安は次のとおりです。

C#
// 重複なしで管理したい
HashSet<int> uniqueIds = new HashSet<int>();

// 順番どおりに管理したい
List<int> orderedIds = new List<int>();

HashSetListはどちらが優れているというより、目的が異なるコレクションです。

6. HashSetでよく使うメソッド一覧

6-1. Add:要素を追加する

Addは、HashSetに要素を追加するメソッドです。

C#
var set = new HashSet<string>();

bool added = set.Add("C#");

Console.WriteLine(added); // True

同じ値をもう一度追加すると、追加されずにfalseが返ります。

C#
bool addedAgain = set.Add("C#");

Console.WriteLine(addedAgain); // False

重複チェックにも使える便利なメソッドです。

6-2. Remove:要素を削除する

Removeは、指定した要素を削除します。

C#
var set = new HashSet<int> { 1, 2, 3 };

bool removed = set.Remove(2);

Console.WriteLine(removed); // True
Console.WriteLine(set.Contains(2)); // False

存在しない要素を削除しようとするとfalseが返ります。

C#
Console.WriteLine(set.Remove(999)); // False

6-3. Contains:要素が存在するか確認する

Containsは、要素が存在するかどうかを確認するメソッドです。

C#
var set = new HashSet<string> { "apple", "banana" };

Console.WriteLine(set.Contains("apple")); // True
Console.WriteLine(set.Contains("orange")); // False

大量データの検索では、List<T>よりHashSet<T>Containsが有利になることが多いです。

6-4. Clear:すべての要素を削除する

Clearは、HashSetの要素をすべて削除します。

C#
var set = new HashSet<int> { 1, 2, 3 };

set.Clear();

Console.WriteLine(set.Count); // 0

同じHashSetインスタンスを再利用したい場合に使えます。

6-5. UnionWith:和集合を作る

UnionWithは、別のコレクションの要素を追加して和集合を作ります。

C#
var setA = new HashSet<int> { 1, 2, 3 };
var setB = new HashSet<int> { 3, 4, 5 };

setA.UnionWith(setB);

foreach (var number in setA)
{
Console.WriteLine(number);
}

結果は、1, 2, 3, 4, 5のように両方の集合を合わせた内容になります。

6-6. IntersectWith:積集合を作る

IntersectWithは、共通する要素だけを残します。

C#
var setA = new HashSet<int> { 1, 2, 3 };
var setB = new HashSet<int> { 2, 3, 4 };

setA.IntersectWith(setB);

foreach (var number in setA)
{
Console.WriteLine(number);
}

結果は2, 3です。

2つのリストの共通要素を求めたいときにも便利です。

6-7. ExceptWith:差集合を作る

ExceptWithは、指定した集合に含まれる要素を取り除きます。

C#
var setA = new HashSet<int> { 1, 2, 3, 4 };
var setB = new HashSet<int> { 3, 4 };

setA.ExceptWith(setB);

foreach (var number in setA)
{
Console.WriteLine(number);
}

結果は1, 2です。

「AにはあるがBにはないもの」を求めたいときに使います。

6-8. SetEquals:集合が等しいか判定する

SetEqualsは、2つの集合が同じ要素を持っているか判定します。

C#
var setA = new HashSet<int> { 1, 2, 3 };
var setB = new HashSet<int> { 3, 2, 1 };

Console.WriteLine(setA.SetEquals(setB)); // True

順番が違っていても、含まれる要素が同じならtrueになります。

C#
var setC = new HashSet<int> { 1, 2, 4 };

Console.WriteLine(setA.SetEquals(setC)); // False

順序ではなく、要素の一致を確認したい場合に使えます。

7. HashSetの集合演算の使い方

7-1. 和集合を求める

和集合とは、2つの集合のどちらかに含まれる要素をまとめたものです。

C#
var a = new HashSet<int> { 1, 2, 3 };
var b = new HashSet<int> { 3, 4, 5 };

a.UnionWith(b);

foreach (var value in a)
{
Console.WriteLine(value);
}

結果は次のような内容になります。

C#
1
2
3
4
5

UnionWithは呼び出し元のHashSetを変更します。元の集合を残したい場合はコピーを作ってから実行します。

C#
var result = new HashSet<int>(a);
result.UnionWith(b);

7-2. 積集合を求める

積集合とは、両方の集合に共通して含まれる要素のことです。

C#
var a = new HashSet<string> { "A", "B", "C" };
var b = new HashSet<string> { "B", "C", "D" };

a.IntersectWith(b);

foreach (var value in a)
{
Console.WriteLine(value);
}

結果は次のようになります。

C#
B
C

共通するID、共通するタグ、共通する権限などを求めるときに使えます。

7-3. 差集合を求める

差集合とは、一方には存在し、もう一方には存在しない要素を求める操作です。

C#
var currentUsers = new HashSet<int> { 1, 2, 3, 4 };
var deletedUsers = new HashSet<int> { 2, 4 };

currentUsers.ExceptWith(deletedUsers);

foreach (var userId in currentUsers)
{
Console.WriteLine(userId);
}

結果は次のようになります。

C#
1
3

「削除対象を除いた一覧」や「未処理のデータ」を求めるときに便利です。

7-4. 対称差を求める

対称差とは、どちらか一方にだけ含まれる要素を求める操作です。

HashSetではSymmetricExceptWithを使います。

C#
var a = new HashSet<int> { 1, 2, 3 };
var b = new HashSet<int> { 3, 4, 5 };

a.SymmetricExceptWith(b);

foreach (var value in a)
{
Console.WriteLine(value);
}

結果は次のようになります。

C#
1
2
4
5

3は両方に含まれているため、結果から除外されます。

7-5. 実務で使える集合演算の例

実務では、2つのリストを比較して「追加されたもの」「削除されたもの」「共通しているもの」を求める場面があります。

たとえば、前回のユーザーID一覧と今回のユーザーID一覧を比較します。

C#
var oldIds = new HashSet<int> { 1, 2, 3, 4 };
var newIds = new HashSet<int> { 3, 4, 5, 6 };

// 追加されたID
var addedIds = new HashSet<int>(newIds);
addedIds.ExceptWith(oldIds);

// 削除されたID
var removedIds = new HashSet<int>(oldIds);
removedIds.ExceptWith(newIds);

// 継続して存在するID
var commonIds = new HashSet<int>(oldIds);
commonIds.IntersectWith(newIds);

Console.WriteLine("追加:");
foreach (var id in addedIds)
{
Console.WriteLine(id);
}

Console.WriteLine("削除:");
foreach (var id in removedIds)
{
Console.WriteLine(id);
}

Console.WriteLine("共通:");
foreach (var id in commonIds)
{
Console.WriteLine(id);
}

このように、HashSetを使うとリスト同士の比較処理をわかりやすく書けます。

8. HashSetで独自クラスの重複判定を行う方法

8-1. 独自クラスをHashSetに入れるときの注意点

intstringのような基本的な型は、HashSetで自然に重複判定できます。

C#
var set = new HashSet<int>();

set.Add(1);
set.Add(1);

Console.WriteLine(set.Count); // 1

しかし、独自クラスの場合は注意が必要です。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}

次のコードでは、同じIdNameを持っていても別のインスタンスとして扱われる可能性があります。

C#
var users = new HashSet<User>();

users.Add(new User { Id = 1, Name = "Alice" });
users.Add(new User { Id = 1, Name = "Alice" });

Console.WriteLine(users.Count); // 2

これは、HashSetが「何をもって同じとするか」を判断できないためです。

独自クラスで正しく重複判定したい場合は、比較ルールを明確にする必要があります。

8-2. EqualsとGetHashCodeを実装する

独自クラスで重複判定を行う方法の1つが、EqualsGetHashCodeを実装する方法です。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";

public override bool Equals(object? obj)
{
if (obj is not User other)
{
return false;
}

return Id == other.Id;
}

public override int GetHashCode()
{
return Id.GetHashCode();
}
}

この例では、Idが同じなら同じユーザーとみなします。

C#
var users = new HashSet<User>();

users.Add(new User { Id = 1, Name = "Alice" });
users.Add(new User { Id = 1, Name = "Alicia" });

Console.WriteLine(users.Count); // 1

HashSetでは、EqualsGetHashCodeの整合性が重要です。

同じと判定されるオブジェクトは、同じハッシュコードを返す必要があります。

悪い例は次のような実装です。

C#
public override bool Equals(object? obj)
{
return obj is User other && Id == other.Id;
}

public override int GetHashCode()
{
return Name.GetHashCode(); // EqualsではIdを使っているのに、HashCodeではNameを使っている
}

このように、EqualsGetHashCodeで使う基準がズレていると、正しく重複判定できないことがあります。

8-3. IEqualityComparerを使って比較ルールを指定する

クラス自体にEqualsGetHashCodeを実装したくない場合は、IEqualityComparer<T>を使えます。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}

public class UserIdComparer : IEqualityComparer<User>
{
public bool Equals(User? x, User? y)
{
if (x is null || y is null)
{
return false;
}

return x.Id == y.Id;
}

public int GetHashCode(User obj)
{
return obj.Id.GetHashCode();
}
}

この比較ルールをHashSetに渡します。

C#
var users = new HashSet<User>(new UserIdComparer());

users.Add(new User { Id = 1, Name = "Alice" });
users.Add(new User { Id = 1, Name = "Alicia" });
users.Add(new User { Id = 2, Name = "Bob" });

Console.WriteLine(users.Count); // 2

IEqualityComparer<T>を使うと、同じクラスでも用途に応じて比較ルールを変えられます。

たとえば、ある場面ではIdで比較し、別の場面ではEmailで比較する、といった使い分けが可能です。

8-4. 大文字・小文字を区別しない文字列比較

HashSet<string>では、比較ルールを指定することで大文字・小文字を区別しない判定ができます。

C#
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

names.Add("Alice");
names.Add("alice");
names.Add("ALICE");

Console.WriteLine(names.Count); // 1

通常のHashSet<string>では、"Alice""alice"は別の文字列として扱われます。

C#
var normalNames = new HashSet<string>();

normalNames.Add("Alice");
normalNames.Add("alice");

Console.WriteLine(normalNames.Count); // 2

メールアドレスやユーザー名など、大文字・小文字を区別したくない場合は、StringComparer.OrdinalIgnoreCaseを指定すると便利です。

C#
var emails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

emails.Add("USER@example.com");
emails.Add("user@example.com");

Console.WriteLine(emails.Count); // 1

9. HashSetを使うときの注意点

9-1. 要素の順番は保証されない

HashSetは順序を管理するためのコレクションではありません。

C#
var set = new HashSet<int> { 3, 1, 2 };

foreach (var number in set)
{
Console.WriteLine(number);
}

出力順が期待どおりに見えることもありますが、その順番に依存するコードは避けるべきです。

順番が必要な場合は、List<T>を使うか、取り出すときに並べ替えます。

C#
foreach (var number in set.OrderBy(x => x))
{
Console.WriteLine(number);
}

9-2. 同じ値をAddしても追加されない

HashSetでは、同じ値をAddしても追加されません。

C#
var set = new HashSet<string>();

set.Add("C#");
set.Add("C#");

Console.WriteLine(set.Count); // 1

この性質は重複排除には便利ですが、同じ値を複数回記録したい場合には向いていません。

たとえば、投票数やアクセス回数のように「同じ値が何回出たか」を管理したい場合は、Dictionary<T, int>などを使うほうが適しています。

C#
var counts = new Dictionary<string, int>();

string key = "apple";

if (counts.ContainsKey(key))
{
counts[key]++;
}
else
{
counts[key] = 1;
}

9-3. インデックスではアクセスできない

HashSetListのようにインデックスでアクセスできません。

C#
var set = new HashSet<string> { "A", "B", "C" };

// set[0] は使えない

インデックスアクセスが必要な場合は、List<T>に変換します。

C#
var list = set.ToList();

Console.WriteLine(list[0]);

ただし、HashSetからListに変換したときの順序も保証されるとは限りません。順序が必要な場合は、明示的に並べ替えましょう。

C#
var sortedList = set.OrderBy(x => x).ToList();

9-4. 可変オブジェクトを要素にするときの落とし穴

HashSetに独自クラスなどの可変オブジェクトを入れる場合、追加後に比較に使う値を変更すると問題が起きることがあります。

C#
public class User
{
public int Id { get; set; }

public override bool Equals(object? obj)
{
return obj is User other && Id == other.Id;
}

public override int GetHashCode()
{
return Id.GetHashCode();
}
}

次のコードでは、HashSetに追加したあとでIdを変更しています。

C#
var user = new User { Id = 1 };
var users = new HashSet<User>();

users.Add(user);

user.Id = 2;

Console.WriteLine(users.Contains(user)); // 期待どおりに動かない可能性がある

HashSetは追加時のハッシュ値をもとに内部管理します。そのため、追加後にハッシュ値のもとになる値を変更すると、要素を見つけられなくなることがあります。

対策としては、HashSetに入れる要素はできるだけ不変にするのが安全です。

C#
public class User
{
public int Id { get; }

public User(int id)
{
Id = id;
}

public override bool Equals(object? obj)
{
return obj is User other && Id == other.Id;
}

public override int GetHashCode()
{
return Id.GetHashCode();
}
}

9-5. nullを扱う場合の注意点

HashSet<string?>のように、nullを含められる型であれば、nullを要素として扱えます。

C#
var set = new HashSet<string?>();

set.Add(null);
set.Add(null);
set.Add("C#");

Console.WriteLine(set.Count); // 2

nullも重複しない要素として扱われるため、複数回追加しても1つだけです。

ただし、IEqualityComparer<T>を自作する場合は、nullへの対応を忘れないようにしましょう。

C#
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;
}

nullを扱う可能性があるコードでは、NullReferenceExceptionが発生しないように比較処理を書くことが大切です。

10. HashSetの実践サンプル

10-1. 入力値の重複チェック

ユーザーが入力した値に重複がないか確認する例です。

C#
var inputs = new List<string>
{
"apple",
"banana",
"apple",
"orange"
};

var seen = new HashSet<string>();

foreach (var input in inputs)
{
if (!seen.Add(input))
{
Console.WriteLine($"重複しています: {input}");
}
}

Addfalseを返した場合、その値はすでに存在しています。

大文字・小文字を区別しない場合は、次のようにします。

C#
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
C#
seen.Add("Apple");
Console.WriteLine(seen.Add("apple")); // False

10-2. IDリストの重複削除

IDの一覧から重複を削除する例です。

C#
var ids = new List<int>
{
1001,
1002,
1001,
1003,
1002
};

var uniqueIds = new HashSet<int>(ids);

foreach (var id in uniqueIds)
{
Console.WriteLine(id);
}

再びList<int>として扱いたい場合は、次のようにします。

C#
var uniqueIdList = uniqueIds.ToList();

並び順も整えたい場合は、OrderByを使います。

C#
var sortedUniqueIds = uniqueIds.OrderBy(x => x).ToList();

10-3. 禁止ワードの高速検索

禁止ワードのチェックにもHashSetは便利です。

C#
var prohibitedWords = new HashSet<string>
{
"spam",
"ngword",
"blocked"
};

string input = "spam";

if (prohibitedWords.Contains(input))
{
Console.WriteLine("禁止ワードが含まれています");
}

大文字・小文字を区別しない場合は、次のようにします。

C#
var prohibitedWords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"spam",
"ngword",
"blocked"
};

Console.WriteLine(prohibitedWords.Contains("SPAM")); // True

大量の禁止ワードをチェックする場合、List<string>よりもHashSet<string>のほうが効率的です。

10-4. 2つのリストの共通要素を抽出

2つのリストから共通する要素を取り出す例です。

C#
var listA = new List<int> { 1, 2, 3, 4 };
var listB = new List<int> { 3, 4, 5, 6 };

var setA = new HashSet<int>(listA);
setA.IntersectWith(listB);

foreach (var value in setA)
{
Console.WriteLine(value);
}

結果は次のようになります。

C#
3
4

LINQを使う場合は、次のようにも書けます。

C#
var common = listA.Intersect(listB).ToList();

ただし、集合として継続的に操作したい場合はHashSetのメソッドを使うとわかりやすいです。

10-5. 既読・未読データの管理

既読データの管理にもHashSetは使えます。

C#
var readArticleIds = new HashSet<int>();

readArticleIds.Add(101);
readArticleIds.Add(102);

int articleId = 101;

if (readArticleIds.Contains(articleId))
{
Console.WriteLine("この記事は既読です");
}
else
{
Console.WriteLine("この記事は未読です");
}

未読の記事だけを抽出する例です。

C#
var allArticleIds = new List<int> { 101, 102, 103, 104 };
var readArticleIds = new HashSet<int> { 101, 103 };

foreach (var id in allArticleIds)
{
if (!readArticleIds.Contains(id))
{
Console.WriteLine($"未読記事: {id}");
}
}

処理済みデータ、既読データ、登録済みデータなど、「すでに存在するか」を確認したい場面ではHashSetが役立ちます。

11. HashSetに関するよくある質問

11-1. HashSetは順番を保持できますか?

HashSetは順番を保持する目的のコレクションではありません。

C#
var set = new HashSet<int> { 3, 1, 2 };

このHashSetforeachで取り出したとき、常に3, 1, 2の順番になると期待するのは避けましょう。

順序が必要な場合は、List<T>を使うか、取り出すときに並べ替えます。

C#
var ordered = set.OrderBy(x => x).ToList();

追加順を重視するなら、最初からList<T>を選ぶのが基本です。

11-2. HashSetとDictionaryの違いは何ですか?

HashSet<T>は、値だけを管理するコレクションです。

C#
var set = new HashSet<int>();

set.Add(1);
set.Add(2);

一方、Dictionary<TKey, TValue>はキーと値のペアを管理します。

C#
var dictionary = new Dictionary<int, string>();

dictionary.Add(1, "Alice");
dictionary.Add(2, "Bob");

違いを簡単にまとめると次のとおりです。

コレクション管理するもの用途
HashSet<T>値のみ存在チェック、重複排除
Dictionary<TKey, TValue>キーと値IDから名前を取得するような対応表

存在するかどうかだけを知りたいならHashSet、キーに対応する値を取り出したいならDictionaryを使います。

11-3. HashSetとDistinctはどちらを使うべきですか?

一度だけ重複削除したい場合は、Distinctが簡単です。

C#
var unique = list.Distinct().ToList();

一方、データを追加しながら重複を防ぎたい場合は、HashSetが向いています。

C#
var set = new HashSet<string>();

foreach (var item in items)
{
if (set.Add(item))
{
Console.WriteLine("初めての値です");
}
}

使い分けの目安は次のとおりです。

やりたいことおすすめ
既存のリストから重複削除したいDistinct
重複を防ぎながら追加したいHashSet
高速な存在チェックをしたいHashSet
LINQで簡潔に書きたいDistinct

11-4. HashSetは重複した要素を何個持てますか?

HashSetは重複した要素を持てません。

C#
var set = new HashSet<int>();

set.Add(1);
set.Add(1);
set.Add(1);

Console.WriteLine(set.Count); // 1

同じ値は1つだけ保持されます。

同じ値を複数回持ちたい場合は、List<T>を使います。

C#
var list = new List<int>();

list.Add(1);
list.Add(1);
list.Add(1);

Console.WriteLine(list.Count); // 3

また、値ごとの出現回数を管理したい場合は、Dictionary<T, int>を使うのが一般的です。

C#
var counts = new Dictionary<string, int>();

foreach (var item in items)
{
if (counts.ContainsKey(item))
{
counts[item]++;
}
else
{
counts[item] = 1;
}
}

11-5. HashSetは初心者でも使うべきですか?

C#初心者でも、HashSetはぜひ覚えておきたいコレクションです。

最初はList<T>だけでも多くの処理は書けますが、次のような場面ではHashSetを使うとコードが簡単になります。

C#
var set = new HashSet<string>();

if (!set.Add("C#"))
{
Console.WriteLine("重複しています");
}

特に、次の3つの使い方を覚えるだけでも十分実用的です。

メソッド役割
Add追加する。重複ならfalse
Contains存在するか確認する
Remove削除する

最初からすべての集合演算を覚える必要はありません。

まずは「重複を防ぐ」「存在チェックを速くする」という目的で使ってみると理解しやすいです。

まとめ

C#のHashSet<T>は、重複しないデータを扱うための便利なコレクションです。

List<T>との大きな違いは、重複を許さないこと、検索が高速なこと、インデックスアクセスができないことです。

C#
var set = new HashSet<int>();

set.Add(1);
set.Add(1);
set.Add(2);

Console.WriteLine(set.Count); // 2

重複削除をしたい場合は、List<T>からHashSet<T>に変換できます。

C#
var list = new List<int> { 1, 2, 2, 3 };

var unique = new HashSet<int>(list);

存在チェックを高速に行いたい場合にも、HashSetは有効です。

C#
if (unique.Contains(2))
{
Console.WriteLine("存在します");
}

また、UnionWithIntersectWithExceptWithなどを使えば、和集合、積集合、差集合といった集合演算も簡単に書けます。

C#
var a = new HashSet<int> { 1, 2, 3 };
var b = new HashSet<int> { 3, 4, 5 };

a.IntersectWith(b);

一方で、HashSetは順序を保証せず、インデックスアクセスもできません。追加順や位置が重要な場合はList<T>を使いましょう。

C#でcsharp hashsetについて学ぶときは、まず次の使い分けを押さえるのが重要です。

目的使うコレクション
重複をなくしたいHashSet
高速に存在チェックしたいHashSet
順番どおりに管理したいList
インデックスで取り出したいList
キーと値を対応させたいDictionary

HashSet<T>を使いこなせるようになると、重複チェック、ID管理、禁止ワード検索、リスト比較などの処理をよりシンプルで効率的に書けるようになります。