C#において、キーと値のペアを保持するDictionary<TKey, TValue>クラスは、日常的なプログラミングで最も頻繁に利用されるコレクションの一つです。
しかし、指定したキーが存在しない場合にインデクサーを使用して値を取得しようとすると、KeyNotFoundExceptionという例外が発生してしまいます。
これを回避しつつ、パフォーマンスを最大限に引き出す手法として推奨されるのがTryGetValueメソッドです。
本記事では、TryGetValueの基本的な使い方から、パフォーマンス上のメリット、そして実務で役立つ実装パターンまでを徹底的に解説します。
TryGetValueメソッドの基本構造
TryGetValueは、Dictionary内に特定のキーが存在するかどうかを確認し、存在する場合はその値を安全に取得するためのメソッドです。
このメソッドは戻り値としてbool型を返し、値の取得に成功した場合はtrue、失敗した場合はfalseを返します。
取得した値は、引数に指定したoutパラメータに格納される仕組みになっています。
基本的な構文とシグネチャ
このメソッドの定義は以下のようになっています。
public bool TryGetValue (TKey key, out TValue value);
- key: 検索するキーを指定します。
- value: キーが見つかった場合、その値がこの変数に格納されます。見つからなかった場合は、型
TValueの規定値 (参照型ならnull、数値型なら0など) が格納されます。
以前のC#バージョンでは、事前に変数を宣言しておく必要がありましたが、現在のC# (7.0以降) ではインライン宣言を用いることで、より簡潔に記述することが可能です。
基本的な実装例
まずは、最も標準的なTryGetValueの使用例を見てみましょう。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// データの準備
var userScores = new Dictionary<string, int>
{
{ "Alice", 95 },
{ "Bob", 80 },
{ "Charlie", 70 }
};
// 存在するキーの検索
string targetUser = "Alice";
if (userScores.TryGetValue(targetUser, out int score))
{
Console.WriteLine($"{targetUser}のスコアは {score} です。");
}
else
{
Console.WriteLine($"{targetUser}は見つかりませんでした。");
}
// 存在しないキーの検索
string unknownUser = "Dave";
if (userScores.TryGetValue(unknownUser, out int unknownScore))
{
Console.WriteLine($"{unknownUser}のスコアは {unknownScore} です。");
}
else
{
Console.WriteLine($"{unknownUser}は見つかりませんでした。");
}
}
}
Aliceのスコアは 95 です。
Daveは見つかりませんでした。
TryGetValueを使用するパフォーマンス上のメリット
C#でDictionaryから値を取得する方法はいくつかありますが、なぜTryGetValueが推奨されるのでしょうか。
その最大の理由は、ハッシュ値の計算回数を最小限に抑えられる点にあります。
ContainsKeyとインデクサーの組み合わせによる弊害
初心者がよく記述してしまうパターンとして、以下のような「二重検索」があります。
// 非効率な例
if (dictionary.ContainsKey(key))
{
var value = dictionary[key];
// 処理
}
このコードの問題点は、ContainsKeyメソッドで一度ハッシュ値を計算してキーを検索し、その直後のインデクサー (dictionary[key]) で再び同じハッシュ計算と検索を繰り返している点です。
Dictionaryの内部では、キーを検索するたびに複雑な計算が行われるため、この「二重ルックアップ」は負荷が高くなります。
TryGetValueによる最適化
対してTryGetValueは、一度の検索プロセスの中で「キーの存在確認」と「値の抽出」を同時に行います。
| 取得方法 | 検索回数 | 特徴 |
|---|---|---|
| インデクサーのみ | 1回 | キーが存在しないと例外が発生する。 |
| ContainsKey + インデクサー | 2回 | 安全だがパフォーマンスが悪い。 |
| TryGetValue | 1回 | 安全かつ高速。最も効率的。 |
大規模なデータを扱うループ内や、非常に高い頻度で呼び出されるAPIの内部などでは、この1回分の検索コストの差がシステム全体のパフォーマンスに大きな影響を与えます。
例外を防ぐための安全な実装パターン
プログラミングにおいて例外処理 (try-catch) はコストが高い処理です。
特にKeyNotFoundExceptionは、プログラムのロジックとして回避可能なものであるため、例外を発生させてから処理するのではなく、TryGetValueを用いて未然に防ぐ (Tester-Doerパターンに近い考え方)ことがベストプラクティスとされています。
規定値を扱うパターン
特定のキーが存在しない場合に、独自のデフォルト値を返したい場合があります。
このような場合もTryGetValueが有用です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var settings = new Dictionary<string, string>
{
{ "Theme", "Dark" },
{ "Language", "Japanese" }
};
// キーがなければ "Default" を使用する
string key = "FontSize";
if (!settings.TryGetValue(key, out var value))
{
value = "Default";
}
Console.WriteLine($"{key} の設定値: {value}");
}
}
FontSize の設定値: Default
LINQのGetValueOrDefaultとの違い
.NETの比較的新しいバージョンでは、GetValueOrDefaultという拡張メソッドも利用できます。
これは内部的にTryGetValueを呼び出していますが、より宣言的に記述できます。
// GetValueOrDefaultの例
var value = settings.GetValueOrDefault("FontSize", "Default");
ただし、値が見つからなかった場合に「追加のロジック (ログ出力や新しい値の追加など)」を実行したい場合は、if (TryGetValue...)の形式の方が柔軟に対応できます。
実践的な応用:値の更新と追加
TryGetValueは、単に値を取得するだけでなく、「値が存在すれば更新し、存在しなければ初期値を設定する」といった集計処理などでも力を発揮します。
カウント処理の最適化
例えば、文章中の単語の出現回数をカウントする処理を考えてみましょう。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
string[] words = { "apple", "banana", "apple", "cherry", "banana", "apple" };
var counts = new Dictionary<string, int>();
foreach (var word in words)
{
// TryGetValueで現在の値を取得
if (counts.TryGetValue(word, out int currentCount))
{
// 存在すれば加算
counts[word] = currentCount + 1;
}
else
{
// 存在しなければ1をセット
counts[word] = 1;
}
}
foreach (var kvp in counts)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}回");
}
}
}
apple: 3回
banana: 2回
cherry: 1回
このパターンでは、ContainsKeyを使うよりも効率的に、かつ簡潔に辞書の内容を更新していくことができます。
参照型とNullable Reference Typesへの対応
C# 8.0以降で導入されたNull許容参照型 (Nullable Reference Types)を使用している場合、TryGetValueの振る舞いには注意が必要です。
もしDictionaryの値が参照型 (stringなど) で、かつキーが見つからなかった場合、outパラメータにはnullが代入されます。
コンパイラはこの事実を認識しているため、if (TryGetValue(...))のブロックの外側でその変数を使用しようとすると警告を出すことがあります。
Dictionary<int, string> names = new();
// names.TryGetValue(1, out string? name) のように宣言することで、
// nullが返る可能性があることを明示的に扱います。
if (names.TryGetValue(1, out var name))
{
// ここでは name は non-null として扱える
Console.WriteLine(name.Length);
}
out varによる宣言を行う際、コンパイラはメソッドの属性 (MaybeNullWhenAttributeなど) を解釈し、戻り値がtrueの場合のみ変数がnullではないことを保証してくれます。
これにより、安全なコードを記述しやすくなっています。
高度な利用シーン:ConcurrentDictionaryでのTryGetValue
マルチスレッド環境において、複数のスレッドから同時にDictionaryへアクセスする場合、標準のDictionaryはスレッドセーフではありません。
そのような場面ではConcurrentDictionary<TKey, TValue>を使用しますが、ここでもTryGetValueは重要な役割を果たします。
ConcurrentDictionaryにおけるTryGetValueは、内部的にロック処理やアトミックな操作を適切に行い、スレッド間の競合を抑えつつ安全に値を取得します。
using System.Collections.Concurrent;
var concurrentMap = new ConcurrentDictionary<int, string>();
concurrentMap.TryAdd(1, "ThreadSafeValue");
if (concurrentMap.TryGetValue(1, out var val))
{
// スレッドセーフに取得可能
Console.WriteLine(val);
}
通常のDictionaryと同様のインターフェースで利用できるため、非同期処理やマルチスレッドプログラミングが必要なシーンでも、TryGetValueの知識はそのまま活かされます。
TryGetValueを使用すべきでないケース
非常に稀なケースですが、TryGetValueよりもインデクサーが適している場面もあります。
それは、「指定したキーがDictionaryに存在しないことが、プログラム上の致命的なバグである場合」です。
もしキーが存在しないことが「想定外」であり、そのまま処理を続行すべきでないならば、インデクサーを使用して明示的にKeyNotFoundExceptionを発生させ、上位の例外ハンドラでキャッチするか、ログを残して異常終了させる方がデバッグが容易になることがあります。
しかし、通常のビジネスロジックやデータ処理においては、安全性を優先して TryGetValue を選択するのが定石と言えるでしょう。
まとめ
C#のDictionary.TryGetValueは、単に「エラーを回避する」ための手段ではなく、コードの可読性、安全性、そして実行速度を同時に向上させる極めて重要なメソッドです。
本記事のポイントをまとめると以下の通りです。
- 例外の回避: キーが存在しない場合でも
KeyNotFoundExceptionを発生させず、戻り値で安全に判定できる。 - 高速な動作: ハッシュ値の計算と検索を一度に済ませるため、
ContainsKeyとインデクサーを併用するより効率的。 - モダンな記述: C# 7.0以降の
out var宣言により、コードを冗長にせずに記述可能。 - 柔軟な対応: 規定値の設定やカウント処理、スレッドセーフなコレクション操作まで幅広く応用できる。
Dictionaryを操作するコードを書く際は、まず「インデクサーを直接使って良いのか」を自問し、少しでもキーが存在しない可能性があるならば、積極的にTryGetValueを採用することをお勧めします。
日々のコーディングにこのパターンを取り入れるだけで、堅牢でパフォーマンスの高いC#アプリケーションの構築に繋がるはずです。
