C#でアプリケーションを開発している際、System.ArgumentException というエラーに遭遇することは珍しくありません。
その中でも「An item with the same key has already been added.(同じ鍵を持つ項目が既に追加されています)」というメッセージは、非常に頻繁に発生する問題の一つです。
この例外は、主に Dictionary<TKey, TValue> などのキーと値のペアを管理するコレクションにおいて、重複したキーを登録しようとしたときに発生します。
エラーが発生する原因を正しく理解し、適切な回避策を講じることは、堅牢なコードを書くための第一歩となります。
この記事では、この例外の発生メカニズムから、実務で使える解決策、そして未然に防ぐためのベストプラクティスまでを詳しく解説します。
例外が発生する主な原因
この例外が発生する最も一般的な理由は、Dictionary クラスの Add メソッドを使用して既に存在するキーを追加しようとしたことにあります。
Dictionary はハッシュテーブルを利用してデータを管理しており、各キーは一意(ユニーク)である必要があります。
以下のコード例は、この例外を意図的に発生させる単純なパターンを示しています。
// 例外が発生するコードの例
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var settings = new Dictionary<string, string>();
// 1回目の追加は成功する
settings.Add("Theme", "Dark");
// 2回目、同じキー "Theme" を追加しようとすると ArgumentException が発生する
settings.Add("Theme", "Light");
}
}
Unhandled exception. System.ArgumentException: An item with the same key has already been added. Key: Theme
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at Program.Main()
このように、一度登録されたキーに対して Add メソッドを再度呼び出すと、ランタイムはデータの整合性を保つために例外をスローします。
実際の開発現場では、ループ処理の中や、外部データの読み込み処理などで意図せずキーが重複してしまうケースが多く見られます。
基本的な解決策と書き換えパターン
エラーを回避するためには、プログラムの要件に応じて適切な方法を選択する必要があります。
ここでは、代表的な 4 つの対処法を紹介します。
1. ContainsKey メソッドによる事前チェック
最も直感的で伝統的な方法は、ContainsKey メソッドを使用してキーの存在を確認することです。
この方法は、既存のデータがある場合には追加せず、ない場合のみ追加したいというロジックに適しています。
if (!settings.ContainsKey("Theme"))
{
settings.Add("Theme", "Dark");
}
else
{
// 既に存在する場合の処理を記述
Console.WriteLine("キーは既に登録されています。");
}
2. インデクサーによる値の上書き
もし「キーが既に存在する場合は、新しい値で上書きしたい」という要件であれば、Add メソッドではなくインデクサー( [ ] )を使用するのが正解です。
インデクサーを使用すると、キーが存在しない場合は新規追加され、存在する場合には既存の値が更新されます。
// この書き方なら、キーが重複していても例外は発生せず、値が "Light" に更新される
settings["Theme"] = "Light";
3. TryAdd メソッドの活用
.NET Core 以降や .NET 5/6/7/8 などのモダンな環境では、TryAdd メソッドを使用するのが効率的です。
TryAdd は、キーの追加に成功したかどうかを bool 値で返してくれるため、条件分岐と追加処理を一行で簡潔に記述できるメリットがあります。
if (settings.TryAdd("Theme", "Blue"))
{
Console.WriteLine("追加に成功しました。");
}
else
{
Console.WriteLine("重複していたため追加されませんでした。");
}
4. TryGetValue メソッドによる取得と更新の統合
既存の値を確認した上で、何らかの演算を行ってから更新したい場合は TryGetValue を使用します。
これにより、辞書の検索を 2 回(存在確認と値の取得)行う手間を省き、パフォーマンスを向上させることができます。
if (settings.TryGetValue("Theme", out string currentValue))
{
// 既存の値を利用した処理
settings["Theme"] = currentValue + "_Updated";
}
LINQ を使用する際の注意点
この例外は、Add メソッドを直接呼び出すとき以外に、LINQ の ToDictionary メソッドを実行した際にもよく発生します。
ソースとなるコレクション内に重複する要素が含まれている場合、ToDictionary は内部で Add を呼び出すため、例外をスローします。
var list = new List<string> { "Apple", "Banana", "Apple" };
// ここで ArgumentException が発生する
var dict = list.ToDictionary(item => item);
このようなケースでは、事前に Distinct を使用して重複を排除するか、GroupBy を使用してグループ化することを検討してください。
また、重複がある場合にどちらを優先するか決まっている場合は、以下のように記述することで回避可能です。
// 重複がある場合に最初の要素を採用する例
var dict = list.GroupBy(x => x).ToDictionary(g => g.Key, g => g.First());
文字列をキーにする場合の落とし穴
文字列をキーとして使用する場合、大文字と小文字の区別が原因でバグを引き起こすことがあります。
デフォルトでは “Key” と “key” は別のキーとして扱われますが、要件によってはこれらを同一視したい場合があるでしょう。
もし意図せずに大文字・小文字違いのキーが混入し、後から正規化(すべて小文字にするなど)して辞書に詰め直そうとすると、この例外が発生します。
このようなトラブルを防ぐには、Dictionary を初期化する際に StringComparer を指定するのが有効です。
// 大文字小文字を区別しない辞書の作成
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
dict.Add("ABC", "123");
// ここで例外は発生せず、"abc" は "ABC" と同じキーとみなされる
// ただし、Addを使用すると重複エラーになるため、構成に応じて使い分ける
マルチスレッド環境での注意点
複数のスレッドから同時に Dictionary への追加操作を行うと、たとえ事前にチェックをしていても例外が発生したり、内部構造が破損したりする恐れがあります。
非同期処理(async/await)や並列処理(Parallel.ForEachなど)の中で辞書を操作する場合は、ConcurrentDictionary<TKey, TValue> の使用を強く推奨します。
ConcurrentDictionary であれば、GetOrAdd や AddOrUpdate といったスレッドセーフなメソッドが提供されており、安全に重複をハンドリングできます。
using System.Collections.Concurrent;
var concurrentDict = new ConcurrentDictionary<string, int>();
// スレッドセーフに値を更新または追加
concurrentDict.AddOrUpdate("Counter", 1, (key, oldValue) => oldValue + 1);
例外を未然に防ぐ設計のポイント
プログラムの構造自体を見直すことで、重複エラーのリスクを最小限に抑えることが可能です。
以下の表に、シチュエーションに応じた推奨されるアプローチをまとめました。
| シチュエーション | 推奨されるアプローチ |
|---|---|
| 設定値などの読み込み | インデクサー( [ ] )を使用して常に最新値に更新する |
| 重複をエラーとして検知したい | Add メソッドを使用し、try-catch で適切にハンドリングする |
| 重複がある場合は無視したい | TryAdd メソッドを使用する |
| 1つのキーに複数の値を保持したい | Dictionary<TKey, List<TValue>> などの構造を検討する |
| 大量のデータを変換する | LINQ の GroupBy で事前集計を行う |
特に、データの出所が外部(ユーザー入力、API応答、データベースなど)である場合は、「データは必ず一意である」という仮定を捨てて実装することが重要です。
入力値のバリデーションフェーズで重複を排除しておくことで、ビジネスロジック内での例外発生を抑えることができます。
まとめ
C# の System.ArgumentException: An item with the same key has already been added. は、開発者がコレクションの特性を正しく理解し、適切に操作することで容易に解決できるエラーです。
単純に Add メソッドを TryAdd やインデクサーに置き換えるだけで解決する場合も多いですが、根本的な原因がデータの正規化不足やマルチスレッドによる競合にある可能性も忘れてはいけません。
この記事で紹介した ContainsKey、TryAdd、ConcurrentDictionary などの手法を、用途に合わせて適切に選択してください。
例外に強いコードを書くことは、アプリケーションの信頼性を高めるだけでなく、デバッグ工数の削減にも大きく貢献します。
今後はエラーメッセージを見た際に、どのタイミングでキーが重複したのかを冷静に追い、最適な回避コードを適用できるよう心がけましょう。
