C#を用いた開発において、データをキーと値のペアで管理するDictionary<TKey, TValue>クラスは、最も頻繁に利用されるコレクションの一つです。

高速な検索性能を持つ一方で、要素を追加する際には「キーの重複」や「nullの扱い」といった、実行時エラーを避けるための注意点が存在します。

本記事では、C#のDictionaryに要素を追加する主要な3つの手法であるAddメソッド、インデクサ、TryAddメソッドの違いを徹底的に解説します。

それぞれの動作特性を理解し、プロジェクトの要件に合わせた最適な実装方法を選択できるようになりましょう。

Dictionaryの基本構造と追加操作の重要性

Dictionaryは内部的にハッシュテーブルという仕組みを利用しており、キーを基にして値(Value)を瞬時に特定できる特徴があります。

要素を追加する際、Dictionaryは指定されたキーの「ハッシュ値」を計算し、その値に基づいてメモリ上の格納場所(バケット)を決定します。

この構造上、Dictionaryにおいて「キーの重複」は許容されません

同じキーを持つ要素を二重に登録しようとした場合、プログラムがどのように振る舞うかは使用するメソッドによって異なります。

適切に使い分けができていないと、意図しない例外(エラー)が発生してアプリケーションが強制終了したり、既存のデータが勝手に書き換わったりするトラブルの原因となります。

Addメソッドによる要素の追加

Addメソッドは、Dictionaryに新しい要素を追加するための最も標準的かつ厳格な方法です。

Addメソッドの基本構文

Addメソッドを使用する場合、引数にキーと値を指定します。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Dictionaryの初期化
        Dictionary<int, string> users = new Dictionary<int, string>();

        // Addメソッドによる追加
        users.Add(1, "田中太郎");
        users.Add(2, "佐藤花子");

        foreach (var user in users)
        {
            Console.WriteLine($"ID: {user.Key}, Name: {user.Value}");
        }
    }
}
実行結果
ID: 1, Name: 田中太郎
ID: 2, Name: 佐藤花子

Addメソッドの注意点と例外

Addメソッドの最大の特徴は、既に存在するキーを追加しようとするとArgumentExceptionが発生するという点です。

これは、「データが重複することはあり得ない」という前提のロジックを組む際に非常に有用です。

逆に、重複の可能性がある場合は、事前にContainsKeyメソッドでチェックするか、後述する別の手法を検討する必要があります。

また、キーにnullを渡した場合はArgumentNullExceptionが発生します(値型がキーの場合はnullにできませんが、参照型をキーにする場合は注意が必要です)。

インデクサ([])による要素の追加と更新

C#のDictionaryでは、配列のように[](インデクサ)を使用して要素を操作できます。

インデクサの動作特性

インデクサを使用した追加は、「存在しなければ追加、存在すれば上書き」という動作になります。

これを「Upsert(アップサート)」と呼ぶこともあります。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<string, int> scores = new Dictionary<string, int>();

        // インデクサによる追加
        scores["Math"] = 80;
        scores["English"] = 90;

        // 既存のキーに対して値を代入(上書きされる)
        scores["Math"] = 100;

        Console.WriteLine($"Math score: {scores["Math"]}");
        Console.WriteLine($"English score: {scores["English"]}");
    }
}
実行結果
Math score: 100
English score: 90

インデクサを使用するメリット

インデクサの利点は、コードが簡潔になることと、例外を回避できることです。

キーが既に存在するかどうかを気にする必要がないため、最新の状態にデータを更新し続けたいログの集計や、設定情報の管理などに適しています。

ただし、意図せず既存のデータを書き換えてしまうリスクがあるため、上書きを禁止したいビジネスロジックではAddメソッドの方が安全です。

TryAddメソッドによる安全な追加

.NET Core 2.0(および .NET Standard 2.1)以降では、TryAddメソッドが導入されました。

これは現代的なC#開発において非常に推奨される手法です。

TryAddメソッドの仕組み

TryAddメソッドは、キーが存在しない場合のみ追加を行い、成功したかどうかをbool値で返します。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<int, string> products = new Dictionary<int, string>();

        // 初回追加(成功する)
        bool result1 = products.TryAdd(101, "Laptop");
        
        // 重複追加の試行(失敗するが例外は出ない)
        bool result2 = products.TryAdd(101, "Desktop");

        Console.WriteLine($"Result 1: {result1} (Added: {products[101]})");
        Console.WriteLine($"Result 2: {result2}");
    }
}
実行結果
Result 1: True (Added: Laptop)
Result 2: False

なぜTryAddが推奨されるのか

従来、例外を避けつつ安全に追加を行うには以下のようなコードを書く必要がありました。

C#
if (!dict.ContainsKey(key))
{
    dict.Add(key, value);
}

この書き方には2つの問題があります。

1つは、Dictionary内部で「キーの検索」を2回(ContainsKeyで1回、Addで1回)行うため、パフォーマンスが低下することです。

もう1つは、マルチスレッド環境において「ContainsKeyとAddの間に別のスレッドが要素を追加してしまう」というレースコンディションが発生する可能性があることです。

TryAddを使用すれば、内部的な検索を最小限に抑えつつ、簡潔かつ安全に処理を記述できます。

コレクション初期化子による一括追加

Dictionaryを宣言すると同時に初期データを投入したい場合は、コレクション初期化子を使用するのが最も効率的です。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // コレクション初期化子
        var rankings = new Dictionary<int, string>
        {
            { 1, "Gold" },
            { 2, "Silver" },
            { 3, "Bronze" }
        };

        // インデクサ形式の初期化(C# 6.0以降)
        var settings = new Dictionary<string, string>
        {
            ["Theme"] = "Dark",
            ["Language"] = "Japanese"
        };

        Console.WriteLine($"Rank 1: {rankings[1]}");
        Console.WriteLine($"Theme: {settings["Theme"]}");
    }
}

インデクサ形式の初期化(["Key"] = value)は、内部的にインデクサの代入処理を呼び出すため、万が一初期化リスト内でキーが重複していても、最後の値で上書きされるだけで例外は発生しません。

一方、{ key, value }形式は内部でAddメソッドを呼び出すため、重複があるとコンパイルは通りますが実行時に例外が発生します。

追加方法の比較まとめ

それぞれの追加方法の違いを表にまとめました。

メソッド / 手法キー重複時の動作戻り値主な用途
Add(k, v)例外発生void重複がバグを意味する場合
dict[k] = v値を上書きなし常に最新値に更新したい場合
TryAdd(k, v)何もしないbool安全かつ高速に追加したい場合
コレクション初期化子形式によるなし固定値の初期設定

パフォーマンスを意識した追加のテクニック

大量のデータをDictionaryに追加する場合、メソッドの選択以外にも考慮すべき点があります。

キャパシティの事前指定

Dictionaryは要素が増えるたびに内部メモリを再確保(リサイズ)します。

追加する要素の数が事前におおよそ分かっている場合は、コンストラクタで初期キャパシティを指定することで、パフォーマンスを劇的に向上させることができます。

C#
// 10,000個の要素が入ることが分かっている場合
var largeData = new Dictionary<int, string>(10000);

これにより、内部でのメモリ再割り当て回数が減り、CPU負荷とメモリ消費を抑えることが可能です。

複合的な操作:GetOrAdd

標準のDictionary<TKey, TValue>にはありませんが、スレッドセーフなConcurrentDictionary<TKey, TValue>には、GetOrAddという便利なメソッドがあります。

これは「値があれば取得し、なければ生成して追加する」という操作をアトミック(不可分)に行います。

標準のDictionaryで同様のことを行う場合は、.NET 6から導入されたCollectionsMarshal.GetValueRefOrAddDefaultなどの高度なAPIもありますが、基本的にはTryGetValueAdd(またはインデクサ)を組み合わせて構築します。

実際の開発シーンでの使い分けガイドライン

最後に、プロフェッショナルな現場でどのようにこれらを使い分けるべきかの指針を示します。

「絶対に重複しないはず」のマスターデータ登録

重複が起きないことが保証されているマスターデータは、Add を使用します。

もし重複が発生するならそれはデータソースやロジックの欠陥であり、例外により即座に問題を明示して早期にバグを検出すべきです。

ユーザー入力や外部APIの結果を保持する場合

外部からのデータやユーザー入力は重複し得るため、TryAdd を使用します。

例外でシステムを停止させず、追加に失敗した事実をログに残すなどの制御で対処するのが適切です。

キャッシュや設定値の管理

インデクサ([])を使用します。

同じキーに新しいデータが来たら古いデータを上書きして最新状態を保つのが自然な振る舞いです。

まとめ

C#のDictionaryへの要素追加は、単純に見えて奥が深いテーマです。

かつてはContainsKeyで確認してからAddするという二段構えの手法が一般的でしたが、現在ではTryAddメソッドによる安全な追加や、インデクサによる柔軟な上書き更新が主流となっています。

  • 厳格にエラーを検知したいならAdd
  • 存在チェックと追加をスマートに行いたいならTryAdd
  • 常に最新の値へ更新したいならインデクサ

これらの特性を正しく理解し、実行時エラーに強い堅牢なコードを記述しましょう。

また、パフォーマンスが求められるシーンでは初期キャパシティの指定も忘れずに行うことが、ワンランク上のC#エンジニアへの近道です。