C# Dictionary の要素操作 - 追加・取得・削除・更新

Dictionary はキーと値のペアを管理するコレクションで、要素の追加・取得・削除・更新といった基本操作を頻繁に使います。それぞれの操作には複数の方法があり、状況に応じて使い分けが必要です。

要素の追加

Dictionary に要素を追加する方法は主に 2 つあります。Add メソッドとインデクサーです。

var scores = new Dictionary<string, int>();

// Add メソッド
scores.Add("Alice", 90);
scores.Add("Bob", 85);

// インデクサー
scores["Charlie"] = 78;

Add メソッドは、すでに同じキーが存在する場合に ArgumentException をスローします。一方、インデクサーによる追加はキーが存在しなければ新規追加、存在すれば上書きという動作になります。

Add メソッド

キーの重複時に例外をスローする。重複が許されない場面で使うと、意図しない上書きを防げる。

インデクサー

キーの重複時に値を上書きする。追加と更新を区別しなくてよい場面で便利。

重複チェックを事前に行いたい場合は、TryAdd メソッドも使えます。

var scores = new Dictionary<string, int>();
scores.Add("Alice", 90);

// キーが存在しなければ追加し true を返す
// 存在すれば何もせず false を返す
bool added = scores.TryAdd("Alice", 100);
Console.WriteLine(added);       // False
Console.WriteLine(scores["Alice"]); // 90(上書きされていない)

TryAdd は .NET Core 2.0 以降で利用可能です。例外を発生させずに重複を検知できるため、パフォーマンス面でも有利になります。

要素の取得

キーを指定して値を取得する最もシンプルな方法はインデクサーです。

var scores = new Dictionary<string, int>
{
    ["Alice"] = 90,
    ["Bob"] = 85
};

int aliceScore = scores["Alice"];
Console.WriteLine(aliceScore); // 90

ただし、存在しないキーを指定すると KeyNotFoundException がスローされます。キーの存在が不確かな場合は TryGetValue を使います。

var scores = new Dictionary<string, int>
{
    ["Alice"] = 90,
    ["Bob"] = 85
};

if (scores.TryGetValue("Charlie", out int value))
{
    Console.WriteLine(value);
}
else
{
    Console.WriteLine("キーが見つかりません");
}

TryGetValue はキーの検索を 1 回だけ行います。ContainsKey で確認してからインデクサーで取得する方法だと内部的に 2 回検索が走るため、TryGetValue のほうが効率的です。

// 非効率(検索が 2 回走る)
if (scores.ContainsKey("Alice"))
{
    int s = scores["Alice"];
}

// 効率的(検索が 1 回で済む)
if (scores.TryGetValue("Alice", out int s2))
{
    // s2 を使う
}

.NET 6 以降では GetValueOrDefault も利用できます。キーが見つからなければ型のデフォルト値(int なら 0)を返します。

var scores = new Dictionary<string, int>
{
    ["Alice"] = 90
};

int score = scores.GetValueOrDefault("Charlie");
Console.WriteLine(score); // 0

// デフォルト値を指定することも可能
int score2 = scores.GetValueOrDefault("Charlie", -1);
Console.WriteLine(score2); // -1

要素の削除

Remove メソッドでキーを指定して要素を削除できます。

var scores = new Dictionary<string, int>
{
    ["Alice"] = 90,
    ["Bob"] = 85,
    ["Charlie"] = 78
};

bool removed = scores.Remove("Bob");
Console.WriteLine(removed);     // True
Console.WriteLine(scores.Count); // 2

Remove はキーが存在すれば削除して true、存在しなければ false を返します。存在しないキーを削除しようとしても例外は発生しません。

削除と同時に値を取得したい場合は、out パラメータ付きのオーバーロードを使います。

var scores = new Dictionary<string, int>
{
    ["Alice"] = 90,
    ["Bob"] = 85
};

if (scores.Remove("Alice", out int removedValue))
{
    Console.WriteLine($"削除された値: {removedValue}"); // 削除された値: 90
}

すべての要素を削除するには Clear メソッドを使います。

scores.Clear();
Console.WriteLine(scores.Count); // 0

要素の更新

値の更新にはインデクサーを使います。

var scores = new Dictionary<string, int>
{
    ["Alice"] = 90,
    ["Bob"] = 85
};

scores["Alice"] = 95;
Console.WriteLine(scores["Alice"]); // 95

キーが存在しない場合、インデクサーは新規追加として動作します。「キーがあれば更新、なければ何もしない」という動作にしたい場合は、ContainsKey や TryGetValue と組み合わせます。

// キーが存在するときだけ更新
if (scores.ContainsKey("Alice"))
{
    scores["Alice"] = 95;
}

値をインクリメントするようなパターンもよく使われます。たとえば文字の出現回数をカウントする例です。

string text = "hello world";
var charCount = new Dictionary<char, int>();

foreach (char c in text)
{
    if (charCount.ContainsKey(c))
    {
        charCount[c]++;
    }
    else
    {
        charCount[c] = 1;
    }
}

foreach (var pair in charCount)
{
    Console.WriteLine($"{pair.Key}: {pair.Value}");
}

このパターンは CollectionsMarshal.GetValueRefOrAddDefault を使うとさらに効率的に書けます。ただし、通常の用途では上記の書き方で十分です。

操作ごとの例外と戻り値

各操作の挙動を整理すると以下のようになります。

操作キーが存在する場合キーが存在しない場合
AddArgumentException追加される
TryAddfalse(何もしない)true(追加される)
インデクサー取得値を返すKeyNotFoundException
TryGetValuetrue + 値を返すfalse + default
Removetrue(削除される)false(何もしない)

Add と TryAdd、インデクサー取得と TryGetValue のように、例外を投げる方法と投げない方法のペアが用意されています。キーの存在が保証できる場面では例外版を、不確実な場面では Try 系メソッドを使い分けるのが基本的な考え方です。