C# HashSetの使い方を徹底解説|重複排除・高速検索・Listとの違いまでわかる入門ガイド
はじめに
C#で「重複しないデータを扱いたい」「大量のデータから高速に存在チェックしたい」と思ったときに便利なのが、HashSet<T>です。
HashSet<T>は、同じ値を重複して保持しないコレクションです。たとえば、ユーザーID、商品コード、メールアドレス、タグ、NGワード、ログの重複行など、「一意な値の集まり」を扱う場面でよく使われます。
C#では配列やList<T>を使う機会が多いため、最初はHashSet<T>の使いどころがわかりにくいかもしれません。しかし、Containsによる検索が高速で、重複排除も簡単にできるため、実務では非常に役立つコレクションです。
この記事では、C#のHashSetの基本的な使い方から、Listとの違い、重複排除、集合演算、独自クラスでの重複判定、注意点、実務での活用例まで入門者向けにわかりやすく解説します。
1. C#のHashSetとは?まず押さえたい基本
1-1. HashSet<T>は「重複しない要素」を扱うコレクション
HashSet<T>は、同じ値を重複して保持しないコレクションです。
たとえば、次のように同じ文字列を複数回追加しても、HashSetの中には1つだけ保存されます。
C#var names = new HashSet<string>();
names.Add("Alice");
names.Add("Bob");
names.Add("Alice");
Console.WriteLine(names.Count); // 2
"Alice"を2回追加していますが、Countは2になります。
HashSet<T>のTは型パラメーターです。HashSet<int>なら整数の集合、HashSet<string>なら文字列の集合を表します。
C#HashSet<int> numbers = new HashSet<int>();
HashSet<string> codes = new HashSet<string>();
「重複を許さないリスト」のように見えますが、List<T>とは性質が大きく異なります。特に、順序やインデックスアクセスの扱いが違うため、使い分けが重要です。
1-2. 内部ではハッシュテーブルを使って高速に検索できる
HashSet<T>は、内部的にハッシュテーブルという仕組みを使っています。
ハッシュテーブルでは、要素からハッシュ値を計算し、その値をもとにデータの格納場所を決めます。そのため、ある値が含まれているかどうかを調べるContainsが非常に高速です。
C#var ids = new HashSet<int> { 1001, 1002, 1003 };
if (ids.Contains(1002))
{
Console.WriteLine("存在します");
}
List<T>でContainsを使う場合、基本的には先頭から順番に探します。要素数が多くなるほど検索に時間がかかります。
一方、HashSet<T>はハッシュ値を使って目的の要素に素早くアクセスできるため、大量データの存在確認に向いています。
1-3. HashSetが向いている処理・向いていない処理
HashSet<T>が向いているのは、次のような処理です。
C#// 重複を取り除きたい
var uniqueValues = new HashSet<string>(values);
// 存在チェックを高速にしたい
if (allowedIds.Contains(userId))
{
// 許可されたID
}
// 2つの集合の差分を取りたい
setA.ExceptWith(setB);
具体的には、以下のような場面で便利です。
IDやコードの重複チェック
大量データの存在確認
CSVやログデータの重複排除
NGワードや許可リストの判定
2つのデータ群の差分抽出
集合演算
一方で、次のような処理には向いていません。
登録した順番を厳密に扱いたい
インデックス番号で要素を取り出したい
同じ値を複数回保持したい
並び順を常にソートしておきたい
たとえば、HashSet<T>では次のようなインデックスアクセスはできません。
C#// これはできない
// var first = set[0];
順番やインデックスが重要な場合は、List<T>を使う方が自然です。
1-4. List・Dictionary・SortedSetとのざっくり違い
C#には複数のコレクションがあります。HashSet<T>とよく比較されるのが、List<T>、Dictionary<TKey, TValue>、SortedSet<T>です。
List<T>は、順序を持つ要素の一覧です。重複を許可し、インデックスでアクセスできます。
C#var list = new List<string> { "A", "B", "A" };
Console.WriteLine(list[0]); // A
HashSet<T>は、重複しない要素の集合です。順序は保証されず、インデックスアクセスもできません。
C#var set = new HashSet<string> { "A", "B", "A" };
Console.WriteLine(set.Count); // 2
Dictionary<TKey, TValue>は、キーと値のペアを扱います。キーは重複できません。
C#var dict = new Dictionary<int, string>();
dict[1] = "Apple";
dict[2] = "Orange";
SortedSet<T>は、重複しない要素をソートされた状態で扱うコレクションです。
C#var sorted = new SortedSet<int> { 3, 1, 2 };
foreach (var n in sorted)
{
Console.WriteLine(n); // 1, 2, 3
}
ざっくり使い分けるなら、次のように考えるとわかりやすいです。
| コレクション | 特徴 | 主な用途 |
|---|---|---|
List<T> | 順序あり、重複あり、インデックスアクセス可能 | 一覧データ |
HashSet<T> | 重複なし、検索が高速 | 重複排除、存在確認 |
Dictionary<TKey, TValue> | キーと値のペア、キー検索が高速 | IDからデータを取得 |
SortedSet<T> | 重複なし、ソート済み | 並び順が必要な集合 |
2. HashSetの基本的な使い方
2-1. HashSetを宣言・初期化する方法
HashSet<T>を使うには、通常はSystem.Collections.Generic名前空間を使用します。
C#using System.Collections.Generic;
空のHashSetを作る基本形は次のとおりです。
C#HashSet<string> fruits = new HashSet<string>();
C#の型推論を使えば、次のように書けます。
C#var fruits = new HashSet<string>();
初期値を指定して作成することもできます。
C#var fruits = new HashSet<string>
{
"Apple",
"Orange",
"Banana"
};
List<T>や配列から作ることもできます。
C#var list = new List<string> { "A", "B", "A", "C" };
var set = new HashSet<string>(list);
Console.WriteLine(set.Count); // 3
このように、既存のコレクションをHashSetに渡すと、重複が自動的に取り除かれます。
2-2. Addで要素を追加する
HashSet<T>に要素を追加するには、Addメソッドを使います。
C#var set = new HashSet<string>();
set.Add("Apple");
set.Add("Orange");
set.Add("Banana");
HashSet<T>では、同じ値を追加しても重複しません。
C#var set = new HashSet<string>();
set.Add("Apple");
set.Add("Apple");
Console.WriteLine(set.Count); // 1
Addメソッドは、追加に成功したかどうかをboolで返します。
C#var set = new HashSet<string>();
bool added1 = set.Add("Apple");
bool added2 = set.Add("Apple");
Console.WriteLine(added1); // True
Console.WriteLine(added2); // False
すでに存在する値を追加しようとした場合、Addはfalseを返します。この性質を使うと、重複チェックを簡単に実装できます。
2-3. Containsで要素の存在を確認する
要素が含まれているか確認するには、Containsメソッドを使います。
C#var set = new HashSet<int> { 1, 2, 3 };
if (set.Contains(2))
{
Console.WriteLine("2は含まれています");
}
Containsは、指定した要素が存在すればtrue、存在しなければfalseを返します。
C#Console.WriteLine(set.Contains(1)); // True
Console.WriteLine(set.Contains(9)); // False
HashSet<T>のContainsは高速なため、大量データの存在確認に向いています。
たとえば、許可されたIDだけ処理したい場合は次のように書けます。
C#var allowedIds = new HashSet<int> { 101, 102, 103 };
int userId = 102;
if (allowedIds.Contains(userId))
{
Console.WriteLine("処理対象です");
}
2-4. Removeで要素を削除する
要素を削除するには、Removeメソッドを使います。
C#var set = new HashSet<string> { "Apple", "Orange", "Banana" };
set.Remove("Orange");
Console.WriteLine(set.Contains("Orange")); // False
Removeもboolを返します。削除できた場合はtrue、指定した要素が存在しなかった場合はfalseです。
C#var set = new HashSet<string> { "Apple" };
bool removed1 = set.Remove("Apple");
bool removed2 = set.Remove("Orange");
Console.WriteLine(removed1); // True
Console.WriteLine(removed2); // False
存在しない要素を削除しようとしても例外は発生しません。
2-5. Countで要素数を取得する
HashSet<T>の要素数は、Countプロパティで取得できます。
C#var set = new HashSet<string> { "A", "B", "C" };
Console.WriteLine(set.Count); // 3
重複する要素を追加しても、Countは増えません。
C#var set = new HashSet<string>();
set.Add("A");
set.Add("A");
set.Add("A");
Console.WriteLine(set.Count); // 1
Countは、重複排除後の一意な要素数を確認したいときにも便利です。
C#var values = new List<string> { "A", "B", "A", "C", "B" };
var uniqueValues = new HashSet<string>(values);
Console.WriteLine(uniqueValues.Count); // 3
2-6. Clearですべての要素を削除する
HashSet<T>の中身をすべて削除するには、Clearメソッドを使います。
C#var set = new HashSet<int> { 1, 2, 3 };
set.Clear();
Console.WriteLine(set.Count); // 0
Clearを実行すると、すべての要素が削除されます。
一時的な作業用のHashSetを使い回したい場合などに便利です。
C#var processedIds = new HashSet<int>();
processedIds.Add(1);
processedIds.Add(2);
// 処理が終わったのでリセット
processedIds.Clear();
2-7. foreachでHashSetの中身を取り出す
HashSet<T>の要素は、foreachで取り出せます。
C#var set = new HashSet<string>
{
"Apple",
"Orange",
"Banana"
};
foreach (var item in set)
{
Console.WriteLine(item);
}
ただし、HashSet<T>は順序を保証しません。
C#var set = new HashSet<int> { 3, 1, 2 };
foreach (var n in set)
{
Console.WriteLine(n);
}
上記の出力順が3, 1, 2になるとは限りません。実行環境や内部状態によって変わる可能性があります。
順序が必要な場合は、OrderByで並べ替えてから取り出します。
C#foreach (var n in set.OrderBy(x => x))
{
Console.WriteLine(n);
}
3. HashSetで重複を排除する方法
3-1. Addの戻り値で重複チェックする
HashSet<T>のAddメソッドは、要素を追加できたかどうかをboolで返します。
この戻り値を使うと、「初めて出てきた値か」「すでに出てきた値か」を簡単に判定できます。
C#var seen = new HashSet<string>();
var values = new List<string>
{
"A",
"B",
"A",
"C",
"B"
};
foreach (var value in values)
{
if (seen.Add(value))
{
Console.WriteLine($"{value} は初めてです");
}
else
{
Console.WriteLine($"{value} は重複です");
}
}
出力例です。
A は初めてです
B は初めてです
A は重複です
C は初めてです
B は重複です
Containsで確認してからAddすることもできますが、重複チェックだけならAddの戻り値を使う方が簡潔です。
C#// やや冗長
if (!seen.Contains(value))
{
seen.Add(value);
}
// シンプル
if (seen.Add(value))
{
// 初めて追加された
}
3-2. ListからHashSetに変換して重複を削除する
List<T>の重複を削除したい場合は、HashSet<T>に変換するだけで簡単に一意な値にできます。
C#var list = new List<int> { 1, 2, 2, 3, 3, 3, 4 };
var set = new HashSet<int>(list);
foreach (var n in set)
{
Console.WriteLine(n);
}
HashSetは重複を保持しないため、1, 2, 3, 4だけが残ります。
文字列でも同じです。
C#var emails = new List<string>
{
"a@example.com",
"b@example.com",
"a@example.com"
};
var uniqueEmails = new HashSet<string>(emails);
Console.WriteLine(uniqueEmails.Count); // 2
ただし、HashSet<T>は順序を保証しません。元のListの順序を保ったまま重複を削除したい場合は、LINQのDistinctを使う方がわかりやすい場合があります。
3-3. HashSetからListへ戻す方法
HashSet<T>に変換して重複を削除したあと、再び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();
または、List<T>のコンストラクターにHashSet<T>を渡すこともできます。
C#var uniqueList = new List<int>(set);
並び順を指定したい場合は、OrderByを組み合わせます。
C#var uniqueSortedList = set.OrderBy(x => x).ToList();
文字列をアルファベット順に並べたい場合は次のようにします。
C#var names = new HashSet<string> { "Charlie", "Alice", "Bob" };
var sortedNames = names.OrderBy(x => x).ToList();
foreach (var name in sortedNames)
{
Console.WriteLine(name);
}
3-4. LINQのDistinctとの違い
重複排除といえば、LINQのDistinctもよく使われます。
C#var list = new List<int> { 1, 2, 2, 3, 3, 4 };
var unique = list.Distinct().ToList();
HashSet<T>とDistinctはどちらも重複排除に使えますが、目的が少し異なります。
Distinctは、シーケンスから重複を除いた結果を取得するためのLINQメソッドです。
C#var uniqueNames = names.Distinct();
一方、HashSet<T>は重複しない要素の集合を保持するコレクションです。
C#var nameSet = new HashSet<string>(names);
一度だけ重複を削除したいなら、Distinctが読みやすいです。
C#var uniqueList = list.Distinct().ToList();
何度も存在チェックしたい、追加しながら重複を判定したい、集合演算をしたい場合は、HashSet<T>が向いています。
C#var set = new HashSet<int>(list);
if (set.Contains(10))
{
Console.WriteLine("10があります");
}
3-5. 実務でよくある重複排除のサンプルコード
たとえば、CSVから読み込んだ商品コードの重複をチェックするケースを考えます。
C#var productCodes = new List<string>
{
"P001",
"P002",
"P003",
"P002",
"P004",
"P001"
};
var seen = new HashSet<string>();
var duplicated = new List<string>();
foreach (var code in productCodes)
{
if (!seen.Add(code))
{
duplicated.Add(code);
}
}
Console.WriteLine("重複しているコード:");
foreach (var code in duplicated)
{
Console.WriteLine(code);
}
このコードでは、seen.Add(code)がfalseになった場合、その商品コードはすでに登場済みです。
重複を除いた一覧だけが必要なら、次のようにシンプルに書けます。
C#var uniqueCodes = new HashSet<string>(productCodes);
foreach (var code in uniqueCodes)
{
Console.WriteLine(code);
}
メールアドレスの重複排除にも使えます。
C#var emails = new List<string>
{
"user1@example.com",
"user2@example.com",
"user1@example.com"
};
var uniqueEmails = emails
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.ToHashSet();
foreach (var email in uniqueEmails)
{
Console.WriteLine(email);
}
大文字・小文字を無視して重複排除したい場合は、比較ルールを指定します。
C#var emails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
emails.Add("USER@example.com");
emails.Add("user@example.com");
Console.WriteLine(emails.Count); // 1
4. HashSetが高速な理由とパフォーマンス
4-1. HashSetの検索が速い仕組み
HashSet<T>が高速に検索できる理由は、内部でハッシュ値を使っているからです。
通常、List<T>で値を探す場合は、先頭から順番に比較していきます。
C#var list = new List<int> { 10, 20, 30, 40 };
bool exists = list.Contains(30);
要素数が少ないうちは問題ありません。しかし、数万件、数十万件、数百万件になると、毎回先頭から探す処理は重くなります。
一方、HashSet<T>では、値からハッシュ値を計算し、その値をもとに格納場所を探します。
C#var set = new HashSet<int> { 10, 20, 30, 40 };
bool exists = set.Contains(30);
そのため、要素数が増えても、平均的には高速に検索できます。
4-2. Containsの計算量は平均O(1)
HashSet<T>のContainsは、平均的にはO(1)です。
O(1)とは、要素数が増えても処理時間が大きく増えにくいことを意味します。
C#var set = new HashSet<int>();
for (int i = 0; i < 100000; i++)
{
set.Add(i);
}
Console.WriteLine(set.Contains(99999)); // True
このように大量の要素があっても、HashSet<T>の検索は高速です。
ただし、常に絶対にO(1)という意味ではありません。ハッシュの衝突が多い場合など、条件によっては遅くなる可能性があります。
それでも、一般的な用途ではList<T>よりも高速な存在確認が期待できます。
4-3. List.Containsとの速度差
List<T>.Containsは、基本的に要素を順番に探します。そのため、計算量はO(n)です。
C#var list = Enumerable.Range(1, 1000000).ToList();
bool exists = list.Contains(999999);
この場合、目的の値が後ろの方にあると、多くの要素を確認する必要があります。
一方、HashSet<T>.Containsは平均O(1)です。
C#var set = Enumerable.Range(1, 1000000).ToHashSet();
bool exists = set.Contains(999999);
大量データに対して何度もContainsを呼ぶ場合、HashSet<T>に変換してから検索した方が効率的です。
C#var targetIds = Enumerable.Range(1, 1000000).ToList();
var searchIds = new List<int> { 10, 2000, 30000, 999999 };
var targetSet = targetIds.ToHashSet();
foreach (var id in searchIds)
{
if (targetSet.Contains(id))
{
Console.WriteLine($"{id} は存在します");
}
}
ループ内でList.Containsを何度も呼んでいるコードは、HashSetへの置き換えで大きく改善できることがあります。
4-4. 要素数が少ない場合はListでも十分なケース
HashSet<T>は高速ですが、常にList<T>より優れているわけではありません。
要素数が少ない場合は、List<T>でも十分です。
C#var roles = new List<string> { "Admin", "User", "Guest" };
if (roles.Contains("Admin"))
{
Console.WriteLine("管理者です");
}
数件から数十件程度の小さなデータであれば、List<T>の方がシンプルで扱いやすい場合があります。
また、HashSet<T>は内部でハッシュテーブルを管理するため、List<T>よりもメモリを多く使う傾向があります。
そのため、次のような場合はList<T>で十分です。
要素数が少ない
検索回数が少ない
順序が重要
インデックスアクセスしたい
重複を許可したい
パフォーマンス改善を目的にHashSetを使う場合は、「大量データ」「頻繁な存在確認」「重複排除」があるかを基準にするとよいです。
4-5. HashSetを使うとメモリ消費が増える点に注意
HashSet<T>は高速な検索を実現するために、内部でハッシュテーブルを保持しています。
そのため、単純なList<T>よりもメモリ消費が増える場合があります。
C#var list = new List<int>();
var set = new HashSet<int>();
同じ数の要素を保持する場合でも、HashSet<T>はハッシュ管理のための追加情報を持ちます。
大量データを扱う場合、検索速度だけでなくメモリ使用量も考慮する必要があります。
たとえば、数百万件のデータを扱う場合は、HashSet<T>が高速でも、メモリに余裕がないと問題になることがあります。
実務では、次のように判断するとよいです。
検索速度を優先するなら
HashSet<T>順序やメモリ効率を優先するなら
List<T>キーから値を取得したいなら
Dictionary<TKey, TValue>ソート状態を保ちたいなら
SortedSet<T>
5. HashSetとListの違いを徹底比較
5-1. 重複を許可するかどうかの違い
List<T>は重複を許可します。
C#var list = new List<string>();
list.Add("A");
list.Add("A");
list.Add("A");
Console.WriteLine(list.Count); // 3
一方、HashSet<T>は重複を許可しません。
C#var set = new HashSet<string>();
set.Add("A");
set.Add("A");
set.Add("A");
Console.WriteLine(set.Count); // 1
同じ値を複数回保持する必要がある場合は、List<T>を使います。
たとえば、注文履歴、アクセスログ、点数一覧などは、同じ値が複数回登場しても意味があります。
C#var scores = new List<int> { 80, 90, 80, 70 };
一方、ユーザーIDや商品コードのように一意であるべき値には、HashSet<T>が向いています。
C#var userIds = new HashSet<int> { 1001, 1002, 1003 };
5-2. 要素の順序が保証されるかどうかの違い
List<T>は、追加した順序を保持します。
C#var list = new List<string>();
list.Add("A");
list.Add("B");
list.Add("C");
foreach (var item in list)
{
Console.WriteLine(item); // A, B, 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>を使うか、HashSet<T>から取り出すときに並べ替えます。
C#foreach (var item in set.OrderBy(x => x))
{
Console.WriteLine(item);
}
5-3. インデックスアクセスできるかどうかの違い
List<T>はインデックスアクセスができます。
C#var list = new List<string> { "A", "B", "C" };
Console.WriteLine(list[0]); // A
Console.WriteLine(list[1]); // B
HashSet<T>はインデックスアクセスができません。
C#var set = new HashSet<string> { "A", "B", "C" };
// これはできない
// Console.WriteLine(set[0]);
HashSet<T>は「何番目の要素」という考え方ではなく、「その値が含まれているか」を扱うためのコレクションです。
先頭の要素が欲しい場合は、LINQのFirstなどを使うことはできます。
C#var first = set.First();
ただし、順序が保証されないため、「最初に追加した要素」を取得できるとは考えない方が安全です。
5-4. 検索速度・追加・削除の違い
List<T>とHashSet<T>では、検索速度が大きく異なります。
List<T>.Containsは、基本的に先頭から順番に探すためO(n)です。
C#var list = Enumerable.Range(1, 100000).ToList();
bool exists = list.Contains(99999);
HashSet<T>.Containsは平均O(1)です。
C#var set = Enumerable.Range(1, 100000).ToHashSet();
bool exists = set.Contains(99999);
追加については、List<T>もHashSet<T>も多くのケースで高速です。ただし、HashSet<T>は追加時に重複チェックを行います。
削除については、List<T>では対象を探してから削除し、削除後に要素を詰める必要があります。HashSet<T>では、値による削除が高速に行えることが多いです。
ただし、パフォーマンスは要素数、型、比較処理、ハッシュの品質によって変わります。小さなデータでは差がほとんど出ない場合もあります。
5-5. HashSetとListの使い分け早見表
| 比較項目 | List<T> | HashSet<T> |
|---|---|---|
| 重複 | 許可する | 許可しない |
| 順序 | 保持する | 保証しない |
| インデックスアクセス | できる | できない |
Containsの速度 | 遅くなりやすい | 平均的に高速 |
| 重複排除 | 自分で処理が必要 | 自動で排除 |
| メモリ使用量 | 比較的少ない | 多くなりやすい |
| 向いている用途 | 一覧、順序付きデータ | 存在確認、重複排除 |
簡単に判断するなら、次のように考えるとよいです。
順序やインデックスが必要 → List
重複をなくしたい・高速に存在確認したい → HashSet
5-6. ListではなくHashSetを選ぶべき具体例
HashSet<T>を選ぶべき代表例は、重複を許可したくないデータです。
C#var registeredEmails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
registeredEmails.Add("user@example.com");
if (!registeredEmails.Add("USER@example.com"))
{
Console.WriteLine("すでに登録済みのメールアドレスです");
}
また、大量データに対して存在チェックを何度も行う場合もHashSet<T>が有効です。
C#var validProductCodes = products
.Select(p => p.Code)
.ToHashSet();
foreach (var order in orders)
{
if (validProductCodes.Contains(order.ProductCode))
{
Console.WriteLine("有効な商品コードです");
}
}
NGワードや許可リストの判定にも向いています。
C#var ngWords = new HashSet<string>
{
"spam",
"badword"
};
if (ngWords.Contains(input))
{
Console.WriteLine("使用できない単語です");
}
5-7. HashSetではなくListを選ぶべき具体例
HashSet<T>ではなくList<T>を選ぶべきなのは、順序や重複に意味がある場合です。
たとえば、ユーザーの操作履歴は、同じ操作が複数回あっても意味があります。
C#var actions = new List<string>
{
"Login",
"ViewProduct",
"ViewProduct",
"Logout"
};
点数一覧も、同じ点数が複数あることに意味があります。
C#var scores = new List<int> { 80, 90, 80, 70 };
ランキングや表示順が重要なデータもList<T>が向いています。
C#var ranking = new List<string>
{
"Alice",
"Bob",
"Charlie"
};
また、インデックスで要素を取り出したい場合もList<T>を使います。
C#Console.WriteLine(ranking[0]); // 1位
6. HashSetで使える集合演算
6-1. UnionWithで和集合を求める
UnionWithは、2つの集合の和集合を求めるメソッドです。
和集合とは、どちらか一方に含まれる要素をすべて集めたものです。
C#var setA = new HashSet<int> { 1, 2, 3 };
var setB = new HashSet<int> { 3, 4, 5 };
setA.UnionWith(setB);
foreach (var n in setA.OrderBy(x => x))
{
Console.WriteLine(n);
}
出力は次のようになります。
1
2
3
4
5
UnionWithは呼び出し元のHashSetを変更します。
元の集合を残したい場合は、コピーを作ってから実行します。
C#var result = new HashSet<int>(setA);
result.UnionWith(setB);
6-2. IntersectWithで積集合を求める
IntersectWithは、2つの集合の積集合を求めるメソッドです。
積集合とは、両方に含まれる要素だけを集めたものです。
C#var setA = new HashSet<int> { 1, 2, 3 };
var setB = new HashSet<int> { 3, 4, 5 };
setA.IntersectWith(setB);
foreach (var n in setA)
{
Console.WriteLine(n);
}
出力は次のようになります。
3
共通するIDや共通タグを探すときに便利です。
C#var userTags = new HashSet<string> { "C#", "ASP.NET", "SQL" };
var requiredTags = new HashSet<string> { "C#", "Azure" };
userTags.IntersectWith(requiredTags);
foreach (var tag in userTags)
{
Console.WriteLine(tag); // C#
}
6-3. ExceptWithで差集合を求める
ExceptWithは、差集合を求めるメソッドです。
差集合とは、ある集合から別の集合に含まれる要素を取り除いたものです。
C#var setA = new HashSet<int> { 1, 2, 3, 4 };
var setB = new HashSet<int> { 3, 4, 5 };
setA.ExceptWith(setB);
foreach (var n in setA.OrderBy(x => x))
{
Console.WriteLine(n);
}
出力は次のようになります。
1
2
実務では、「既存データに存在しない新規データを抽出する」ような場面で役立ちます。
C#var currentIds = new HashSet<int> { 1, 2, 3, 4 };
var deletedIds = new HashSet<int> { 2, 4 };
currentIds.ExceptWith(deletedIds);
// currentIds は 1, 3 になる
6-4. SymmetricExceptWithで対称差を求める
SymmetricExceptWithは、対称差を求めるメソッドです。
対称差とは、どちらか一方にだけ含まれる要素です。両方に含まれる要素は除外されます。
C#var setA = new HashSet<int> { 1, 2, 3 };
var setB = new HashSet<int> { 3, 4, 5 };
setA.SymmetricExceptWith(setB);
foreach (var n in setA.OrderBy(x => x))
{
Console.WriteLine(n);
}
出力は次のようになります。
1
2
4
5
変更前と変更後のデータを比較し、片方にしか存在しない値を調べるときに便利です。
6-5. IsSubsetOf・IsSupersetOfで包含関係を判定する
IsSubsetOfは、ある集合が別の集合の部分集合かどうかを判定します。
C#var small = new HashSet<int> { 1, 2 };
var large = new HashSet<int> { 1, 2, 3, 4 };
Console.WriteLine(small.IsSubsetOf(large)); // True
IsSupersetOfは、ある集合が別の集合をすべて含んでいるかどうかを判定します。
C#Console.WriteLine(large.IsSupersetOf(small)); // True
たとえば、ユーザーが必要な権限をすべて持っているか確認する場合に使えます。
C#var userPermissions = new HashSet<string>
{
"Read",
"Write",
"Delete"
};
var requiredPermissions = new HashSet<string>
{
"Read",
"Write"
};
if (userPermissions.IsSupersetOf(requiredPermissions))
{
Console.WriteLine("必要な権限を持っています");
}
6-6. SetEqualsで2つの集合が同じか判定する
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です。
List<T>のSequenceEqualとは考え方が異なります。
C#var listA = new List<int> { 1, 2, 3 };
var listB = new List<int> { 3, 2, 1 };
Console.WriteLine(listA.SequenceEqual(listB)); // False
SetEqualsは集合として同じかを判定するため、順序を気にしない比較に向いています。
7. 独自クラスをHashSetで扱う方法
7-1. 独自クラスで重複判定がうまくいかない理由
HashSet<T>でintやstringを扱う場合は、自然に重複判定できます。
C#var set = new HashSet<string>();
set.Add("A");
set.Add("A");
Console.WriteLine(set.Count); // 1
しかし、独自クラスでは注意が必要です。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
次のコードを見てください。
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
見た目は同じユーザーですが、Countは2になります。
これは、クラスのインスタンスは標準では参照が同じかどうかで比較されるためです。つまり、同じ値を持っていても、別々にnewされたオブジェクトは別物として扱われます。
7-2. EqualsとGetHashCodeをオーバーライドする
独自クラスで値による重複判定をしたい場合は、EqualsとGetHashCodeを適切に実装します。
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 = "Alice Updated" });
Console.WriteLine(users.Count); // 1
HashSet<T>では、EqualsだけでなくGetHashCodeも重要です。
同じと判定されるオブジェクトは、同じハッシュコードを返す必要があります。
複数のプロパティで比較したい場合は、HashCode.Combineを使うと便利です。
C#public class Product
{
public string Code { get; set; } = "";
public string Category { get; set; } = "";
public override bool Equals(object? obj)
{
if (obj is not Product other)
{
return false;
}
return Code == other.Code
&& Category == other.Category;
}
public override int GetHashCode()
{
return HashCode.Combine(Code, Category);
}
}
7-3. IEqualityComparer<T>で比較ルールを指定する
クラス自体にEqualsやGetHashCodeを実装したくない場合は、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 = "Alice Updated" });
Console.WriteLine(users.Count); // 1
IEqualityComparer<T>を使うと、用途ごとに比較ルールを切り替えられます。
たとえば、ある場面ではIdで比較し、別の場面ではEmailで比較する、といった設計ができます。
7-4. 大文字・小文字を無視して文字列を比較する
文字列のHashSetでは、大文字・小文字を無視して比較したい場合があります。
たとえば、メールアドレスやコードの重複チェックです。
C#var emails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
emails.Add("USER@example.com");
emails.Add("user@example.com");
Console.WriteLine(emails.Count); // 1
通常のHashSet<string>では、大文字・小文字を区別します。
C#var emails = new HashSet<string>();
emails.Add("USER@example.com");
emails.Add("user@example.com");
Console.WriteLine(emails.Count); // 2
大文字・小文字を無視したい場合は、StringComparer.OrdinalIgnoreCaseを指定しましょう。
C#var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"abc",
"ABC"
};
Console.WriteLine(codes.Count); // 1
文字列比較では、用途に応じて適切なStringComparerを選ぶことが大切です。
7-5. 重複判定でよくある実装ミス
独自クラスをHashSet<T>で扱うときによくあるミスは、Equalsだけを実装してGetHashCodeを実装しないことです。
C#public override bool Equals(object? obj)
{
return obj is User other && Id == other.Id;
}
// GetHashCodeを実装していない
HashSet<T>ではハッシュコードを使って要素を管理するため、EqualsとGetHashCodeはセットで考える必要があります。
また、比較に使うプロパティを後から変更するのも危険です。
C#var user = new User { Id = 1, Name = "Alice" };
var users = new HashSet<User>();
users.Add(user);
// HashSetに追加した後で、比較に使うIdを変更
user.Id = 2;
このように、HashSetに追加した後でハッシュコードに影響する値を変更すると、検索や削除が正しく動かなくなる可能性があります。
比較に使う値は、できるだけ不変にするのが安全です。
C#public class User
{
public int Id { get; }
public string Name { get; }
public User(int id, string name)
{
Id = id;
Name = name;
}
}
8. HashSetを使うときの注意点
8-1. 要素の順序は保証されない
HashSet<T>では、要素の順序は保証されません。
C#var set = new HashSet<int> { 3, 1, 2 };
foreach (var n in set)
{
Console.WriteLine(n);
}
たまたま期待した順序で出力されることがあっても、その順序に依存するコードを書くべきではありません。
順序が必要なら、取り出すときに並べ替えます。
C#foreach (var n in set.OrderBy(x => x))
{
Console.WriteLine(n);
}
追加順を保持したい場合は、List<T>との併用を検討します。
C#var list = new List<string>();
var set = new HashSet<string>();
void AddIfNotExists(string value)
{
if (set.Add(value))
{
list.Add(value);
}
}
このようにすると、重複を防ぎながら追加順も保持できます。
8-2. 同じ値を追加しても要素数は増えない
HashSet<T>では、同じ値を追加しても要素数は増えません。
C#var set = new HashSet<string>();
set.Add("A");
set.Add("A");
Console.WriteLine(set.Count); // 1
これはHashSet<T>の重要な特徴です。
重複した値を追加したときに例外が発生するわけではなく、単に追加されません。
C#bool added = set.Add("A");
if (!added)
{
Console.WriteLine("すでに存在します");
}
重複をエラーとして扱いたい場合は、Addの戻り値をチェックします。
C#if (!set.Add(code))
{
throw new InvalidOperationException($"重複したコードです: {code}");
}
8-3. nullを扱うときの注意点
HashSet<T>では、参照型の場合、nullを要素として扱えます。
C#var set = new HashSet<string?>();
set.Add(null);
set.Add(null);
Console.WriteLine(set.Count); // 1
nullも1つの値として扱われるため、複数回追加しても1つだけです。
ただし、nullを許可する設計にするかどうかは明確にしておくべきです。
C#var names = new HashSet<string?>();
names.Add("Alice");
names.Add(null);
nullを除外したい場合は、追加前にチェックします。
C#if (name is not null)
{
names.Add(name);
}
また、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;
}
8-4. 可変オブジェクトを要素にするリスク
HashSet<T>に可変オブジェクトを入れる場合は注意が必要です。
特に、EqualsやGetHashCodeに使っているプロパティを後から変更すると危険です。
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();
}
}
次のようなコードは問題を起こす可能性があります。
C#var user = new User { Id = 1 };
var users = new HashSet<User>();
users.Add(user);
user.Id = 2;
Console.WriteLine(users.Contains(user)); // 期待通りにならない可能性がある
HashSetに追加した時点のハッシュコードと、変更後のハッシュコードが変わってしまうためです。
安全に使うには、比較に使う値を変更しない設計にします。
C#public class User
{
public int Id { get; }
public User(int id)
{
Id = id;
}
}
または、変更が必要な場合は一度削除してから再追加します。
C#users.Remove(user);
user.Id = 2;
users.Add(user);
8-5. ソートしたい場合はSortedSetやOrderByを使う
HashSet<T>は順序を保証しないため、常にソートされた状態で扱いたい場合には向いていません。
取り出すときだけソートしたいなら、OrderByを使います。
C#var set = new HashSet<int> { 3, 1, 2 };
foreach (var n in set.OrderBy(x => x))
{
Console.WriteLine(n);
}
常にソートされた集合として扱いたい場合は、SortedSet<T>を使います。
C#var sortedSet = new SortedSet<int> { 3, 1, 2 };
foreach (var n in sortedSet)
{
Console.WriteLine(n); // 1, 2, 3
}
SortedSet<T>も重複を許可しませんが、内部構造が異なるため、検索や追加の計算量はHashSet<T>とは異なります。
高速な存在確認を優先するならHashSet<T>、並び順を優先するならSortedSet<T>と考えるとよいです。
8-6. スレッドセーフではない点に注意する
HashSet<T>は、複数のスレッドから同時に書き込みを行う用途にはそのまま使えません。
たとえば、複数スレッドから同時にAddやRemoveを行うと、問題が発生する可能性があります。
C#var set = new HashSet<int>();
// 複数スレッドから同時にset.Add(...)するような使い方には注意
マルチスレッド環境で使う場合は、lockで保護するなどの対策が必要です。
C#var set = new HashSet<int>();
var gate = new object();
lock (gate)
{
set.Add(1);
}
または、用途に応じてスレッドセーフなコレクションを検討します。
読み取りだけであれば問題になりにくいですが、読み取り中に別スレッドで変更される可能性がある場合は注意が必要です。
9. 実務で役立つHashSetの活用例
9-1. IDやコードの重複チェック
実務でよくあるのが、IDやコードの重複チェックです。
C#var codes = new List<string>
{
"A001",
"A002",
"A003",
"A002"
};
var seen = new HashSet<string>();
foreach (var code in codes)
{
if (!seen.Add(code))
{
Console.WriteLine($"重複コード: {code}");
}
}
データ登録前に重複を検出したい場合に便利です。
大文字・小文字を区別しないコードなら、比較ルールを指定します。
C#var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var code in codes)
{
if (!seen.Add(code))
{
Console.WriteLine($"重複コード: {code}");
}
}
9-2. 大量データの存在確認
大量データの存在確認では、HashSet<T>が非常に有効です。
たとえば、注文データの商品コードが、商品マスタに存在するか確認するケースです。
C#var masterProductCodes = products
.Select(p => p.Code)
.ToHashSet();
foreach (var order in orders)
{
if (!masterProductCodes.Contains(order.ProductCode))
{
Console.WriteLine($"存在しない商品コード: {order.ProductCode}");
}
}
List.Containsをループ内で何度も呼ぶと遅くなることがあります。
C#// 遅くなりやすい例
foreach (var order in orders)
{
if (productCodesList.Contains(order.ProductCode))
{
// 処理
}
}
このような場合は、事前にHashSetへ変換します。
C#var productCodeSet = productCodesList.ToHashSet();
foreach (var order in orders)
{
if (productCodeSet.Contains(order.ProductCode))
{
// 処理
}
}
9-3. NGワード・許可リストの判定
NGワードや許可リストの判定にもHashSet<T>は向いています。
C#var ngWords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"spam",
"ngword",
"禁止"
};
string input = "spam";
if (ngWords.Contains(input))
{
Console.WriteLine("使用できない単語です");
}
許可されたステータスだけ受け付ける場合にも使えます。
C#var allowedStatuses = new HashSet<string>
{
"New",
"Processing",
"Completed"
};
if (!allowedStatuses.Contains(status))
{
throw new ArgumentException("不正なステータスです");
}
switchや複数のifで書くよりも、対象が多い場合はHashSetの方が管理しやすくなります。
9-4. CSVやログデータの重複排除
CSVやログデータを処理する際、同じ行や同じキーを重複して処理したくないことがあります。
C#var lines = File.ReadLines("input.csv");
var seenKeys = new HashSet<string>();
foreach (var line in lines)
{
var columns = line.Split(',');
var key = columns[0];
if (!seenKeys.Add(key))
{
Console.WriteLine($"重複行をスキップ: {line}");
continue;
}
// 重複していない行だけ処理
Console.WriteLine($"処理: {line}");
}
ログファイルから一意なエラーコードだけ抽出する場合にも使えます。
C#var errorCodes = new HashSet<string>();
foreach (var line in File.ReadLines("app.log"))
{
if (line.Contains("ERROR"))
{
var code = ExtractErrorCode(line);
errorCodes.Add(code);
}
}
foreach (var code in errorCodes)
{
Console.WriteLine(code);
}
9-5. 2つのデータ群の差分抽出
HashSet<T>は、2つのデータ群の差分抽出にも便利です。
たとえば、旧データと新データを比較し、新しく追加されたIDを求めます。
C#var oldIds = new HashSet<int> { 1, 2, 3 };
var newIds = new HashSet<int> { 2, 3, 4, 5 };
var addedIds = new HashSet<int>(newIds);
addedIds.ExceptWith(oldIds);
foreach (var id in addedIds)
{
Console.WriteLine($"追加されたID: {id}");
}
削除されたIDを求める場合は、逆にします。
C#var removedIds = new HashSet<int>(oldIds);
removedIds.ExceptWith(newIds);
foreach (var id in removedIds)
{
Console.WriteLine($"削除されたID: {id}");
}
共通して存在するIDを求めるなら、IntersectWithを使います。
C#var commonIds = new HashSet<int>(oldIds);
commonIds.IntersectWith(newIds);
9-6. ループ内のContainsを高速化するリファクタリング
実務でよくある改善例として、ループ内のList.ContainsをHashSet.Containsに置き換えるリファクタリングがあります。
改善前のコードです。
C#var targetIds = GetTargetIds(); // List<int>
var users = GetUsers();
foreach (var user in users)
{
if (targetIds.Contains(user.Id))
{
Console.WriteLine(user.Name);
}
}
usersとtargetIdsの件数が多い場合、このコードは遅くなる可能性があります。
改善後は、事前にHashSetへ変換します。
C#var targetIds = GetTargetIds().ToHashSet();
var users = GetUsers();
foreach (var user in users)
{
if (targetIds.Contains(user.Id))
{
Console.WriteLine(user.Name);
}
}
この変更だけで、処理時間が大きく改善されることがあります。
特に、次のようなコードを見つけたらHashSetを検討するとよいです。
C#foreach (var item in largeList)
{
if (anotherLargeList.Contains(item.Id))
{
// 処理
}
}
この場合、anotherLargeListを先にHashSetへ変換することで、検索コストを減らせます。
10. HashSetに関するよくある質問
10-1. HashSetとListはどちらが速い?
用途によります。
存在確認、つまりContainsを大量に行う場合は、一般的にHashSet<T>の方が高速です。
C#var set = ids.ToHashSet();
if (set.Contains(targetId))
{
// 高速に存在確認
}
一方、要素数が少ない場合や、順序を扱いたい場合はList<T>で十分です。
C#var list = new List<string> { "A", "B", "C" };
目安としては、次のように考えるとわかりやすいです。
少量データで順序が必要なら
List<T>大量データで存在確認が多いなら
HashSet<T>重複を許可したいなら
List<T>重複を排除したいなら
HashSet<T>
10-2. HashSetは順番を保持できる?
HashSet<T>は順番を保証しません。
C#var set = new HashSet<int> { 3, 1, 2 };
この集合をforeachで取り出したとき、必ず3, 1, 2の順になるとは限りません。
順番を保持したい場合は、List<T>を使います。
重複を防ぎながら順序も保持したい場合は、HashSet<T>とList<T>を組み合わせる方法があります。
C#var set = new HashSet<string>();
var list = new List<string>();
void Add(string value)
{
if (set.Add(value))
{
list.Add(value);
}
}
ソート順で扱いたいなら、SortedSet<T>やOrderByを使います。
C#var sorted = set.OrderBy(x => x).ToList();
10-3. HashSetで重複した値を追加するとどうなる?
重複した値を追加しても、要素数は増えません。
C#var set = new HashSet<string>();
set.Add("A");
set.Add("A");
Console.WriteLine(set.Count); // 1
例外は発生せず、2回目以降の追加は無視されます。
Addメソッドの戻り値を使うと、追加されたかどうかを判定できます。
C#bool added = set.Add("A");
if (!added)
{
Console.WriteLine("すでに存在しています");
}
重複を検出したい場合は、この戻り値を活用しましょう。
10-4. HashSetとDictionaryはどう使い分ける?
HashSet<T>は、値そのものの存在を管理するコレクションです。
C#var userIds = new HashSet<int>();
userIds.Add(1001);
if (userIds.Contains(1001))
{
Console.WriteLine("存在します");
}
Dictionary<TKey, TValue>は、キーから値を取得するためのコレクションです。
C#var users = new Dictionary<int, string>();
users[1001] = "Alice";
Console.WriteLine(users[1001]); // Alice
使い分けは次のとおりです。
存在するかどうかだけ知りたい → HashSet
キーに対応する値を取得したい → Dictionary
たとえば、ユーザーIDの一覧だけでよいならHashSet<int>です。
C#var activeUserIds = new HashSet<int>();
ユーザーIDからユーザー名を取得したいならDictionary<int, string>です。
C#var userNames = new Dictionary<int, string>();
10-5. HashSetとDistinctはどちらを使うべき?
一度だけ重複を削除してリストにしたい場合は、Distinctが簡単です。
C#var uniqueList = list.Distinct().ToList();
一方、重複しない集合として保持したい場合や、あとで何度もContainsを使う場合は、HashSet<T>が向いています。
C#var uniqueSet = list.ToHashSet();
if (uniqueSet.Contains(target))
{
// 存在確認
}
追加しながら重複チェックしたい場合もHashSet<T>が便利です。
C#var seen = new HashSet<string>();
foreach (var value in values)
{
if (!seen.Add(value))
{
Console.WriteLine($"重複: {value}");
}
}
つまり、使い分けは次のようになります。
重複を取り除いた結果が欲しいだけ → Distinct
重複しない集合として使い続けたい → HashSet
10-6. HashSetで独自クラスの重複判定をするには?
独自クラスで重複判定をしたい場合は、EqualsとGetHashCodeを正しく実装します。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public override bool Equals(object? obj)
{
return obj is User other && 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 = "Alice Updated" });
Console.WriteLine(users.Count); // 1
クラス自体を変更したくない場合は、IEqualityComparer<T>を使います。
C#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();
}
}
使用例です。
C#var users = new HashSet<User>(new UserIdComparer());
独自クラスでは、「何をもって同じとみなすか」を明確にすることが大切です。
まとめ
C#のHashSet<T>は、重複しない要素を扱うための便利なコレクションです。
同じ値を複数回追加しても1つだけ保持され、Containsによる存在確認が平均的に高速です。そのため、重複排除や大量データの検索、IDやコードのチェック、集合演算などでよく使われます。
基本的な使い方はシンプルです。
C#var set = new HashSet<string>();
set.Add("A");
set.Add("B");
Console.WriteLine(set.Contains("A")); // True
Console.WriteLine(set.Count); // 2
List<T>との大きな違いは、重複、順序、インデックスアクセス、検索速度です。
List<T>は順序を保持し、重複を許可し、インデックスでアクセスできます。一方、HashSet<T>は重複を許可せず、順序を保証せず、存在確認を高速に行えます。
順序が必要ならList
重複排除や高速検索が必要ならHashSet
また、HashSet<T>には集合演算も用意されています。
C#setA.UnionWith(setB); // 和集合
setA.IntersectWith(setB); // 積集合
setA.ExceptWith(setB); // 差集合
setA.SymmetricExceptWith(setB); // 対称差
独自クラスを扱う場合は、EqualsとGetHashCode、またはIEqualityComparer<T>を正しく実装する必要があります。
HashSet<T>は非常に便利ですが、順序が保証されない、インデックスアクセスできない、メモリ消費が増える場合がある、可変オブジェクトの扱いに注意が必要といった特徴もあります。
実務では、次のような場面で特に効果を発揮します。
IDやコードの重複チェック
メールアドレスや商品コードの一意化
大量データの存在確認
NGワードや許可リストの判定
CSVやログの重複排除
2つのデータ群の差分抽出
ループ内の
Contains高速化
HashSet<T>を使いこなせるようになると、C#でのデータ処理をよりシンプルかつ効率的に書けるようになります。

