C# TryGetValue の使い方 - 安全に値を取得する
Dictionary からキーを指定して値を取得するとき、キーが存在しなければ KeyNotFoundException が発生します。TryGetValue はこの問題を回避し、例外なしで安全に値を取得できるメソッドです。
インデクサーとの違い
インデクサーでキーを指定すると、キーが見つからない場合に例外がスローされます。
var users = new Dictionary<int, string>
{
[1] = "Alice",
[2] = "Bob"
};
// キーが存在する場合は問題ない
string name = users[1]; // "Alice"
// キーが存在しない場合は例外
string unknown = users[99]; // KeyNotFoundExceptionTryGetValue は戻り値の bool でキーの存在を判定し、out パラメータで値を受け取ります。
var users = new Dictionary<int, string>
{
[1] = "Alice",
[2] = "Bob"
};
if (users.TryGetValue(1, out string name))
{
Console.WriteLine(name); // Alice
}
if (!users.TryGetValue(99, out string notFound))
{
Console.WriteLine("キーが見つかりません");
// notFound は null(参照型のデフォルト値)
}キーが見つからなかった場合、out パラメータには型のデフォルト値が入ります。参照型なら null、int なら 0、bool なら false です。
ContainsKey + インデクサーとの比較
TryGetValue を使わずに、ContainsKey で存在確認してからインデクサーで取得するコードもよく見かけます。
var prices = new Dictionary<string, decimal>
{
["apple"] = 150m,
["banana"] = 100m
};
// ContainsKey + インデクサー(内部で 2 回検索)
if (prices.ContainsKey("apple"))
{
decimal price = prices["apple"];
Console.WriteLine(price);
}
// TryGetValue(内部で 1 回検索)
if (prices.TryGetValue("apple", out decimal p))
{
Console.WriteLine(p);
}ContainsKey + インデクサーの組み合わせでは、Dictionary 内部のハッシュテーブルを 2 回検索することになります。TryGetValue なら 1 回の検索で存在確認と値の取得を同時に行えるため、パフォーマンス上の無駄がありません。
検索が 2 回走る。コードは読みやすいが、ホットパスでは非効率になりうる。
検索が 1 回で済む。out パラメータの構文に慣れが必要だが、標準的なイディオムとして広く使われている。
デフォルト値を返すパターン
キーが見つからなかったときにデフォルト値を使いたいケースは多くあります。TryGetValue と三項演算子を組み合わせると簡潔に書けます。
var config = new Dictionary<string, string>
{
["theme"] = "dark",
["language"] = "ja"
};
// キーがなければ "default" を使う
string theme = config.TryGetValue("theme", out string t) ? t : "default";
string fontSize = config.TryGetValue("font_size", out string f) ? f : "14px";
Console.WriteLine(theme); // dark
Console.WriteLine(fontSize); // 14px.NET 6 以降であれば GetValueOrDefault を使うとさらに短く書けます。
string theme = config.GetValueOrDefault("theme", "default");
string fontSize = config.GetValueOrDefault("font_size", "14px");ただし GetValueOrDefault は IReadOnlyDictionary の拡張メソッドであり、TryGetValue は Dictionary 本体のメソッドという違いがあります。汎用的に使えるのは TryGetValue のほうです。
値型の Dictionary での注意点
値型(int、double など)を値に持つ Dictionary で TryGetValue を使うとき、キーが見つからなかった場合の out パラメータには 0 が入ります。
var scores = new Dictionary<string, int>
{
["Alice"] = 0,
["Bob"] = 85
};
if (scores.TryGetValue("Alice", out int aliceScore))
{
// aliceScore は 0 だが、キーは存在する
Console.WriteLine($"Alice: {aliceScore}");
}
if (!scores.TryGetValue("Charlie", out int charlieScore))
{
// charlieScore も 0 だが、キーは存在しない
Console.WriteLine($"Charlie のデフォルト値: {charlieScore}");
}「値が 0」と「キーが存在しない」を区別するには、戻り値の bool を必ず確認する必要があります。out パラメータの値だけを見ても区別できません。
このように、値型ではデフォルト値と実際の値が一致する可能性があるため、TryGetValue の戻り値(bool)を無視してはいけません。
int なら 0、bool なら false が実データとデフォルト値の両方に該当しうる問題。
パターンマッチングとの組み合わせ
C# 7.0 以降ではインライン変数宣言が可能なので、TryGetValue を if 文の中で直接使えます。さらにパターンマッチングと組み合わせると、取得した値に対する条件分岐も簡潔に書けます。
var users = new Dictionary<int, string>
{
[1] = "Alice",
[2] = "Bob",
[3] = ""
};
// null でも空文字でもない場合だけ処理する
if (users.TryGetValue(1, out string name) && !string.IsNullOrEmpty(name))
{
Console.WriteLine($"ユーザー名: {name}");
}nullable 参照型が有効な環境では、TryGetValue の out パラメータに対してコンパイラが null 可能性の警告を出すことがあります。その場合は null 許容注釈を使って対応します。
var data = new Dictionary<string, string>
{
["key1"] = "value1"
};
// TryGetValue の out パラメータは string? として扱われる
if (data.TryGetValue("key1", out string? value) && value is not null)
{
Console.WriteLine(value.Length); // 安全にアクセスできる
}よくある使用パターン
TryGetValue がよく使われる場面をいくつか紹介します。
キャッシュの参照パターンでは、キーが存在すればキャッシュから返し、なければ計算して追加するという流れになります。
var cache = new Dictionary<string, byte[]>();
byte[] GetOrLoadFile(string path)
{
if (cache.TryGetValue(path, out byte[] data))
{
return data;
}
data = File.ReadAllBytes(path);
cache[path] = data;
return data;
}列挙型の変換パターンでは、文字列から列挙値へのマッピングに Dictionary を使い、TryGetValue で安全に変換します。
var statusMap = new Dictionary<string, HttpStatusCode>
{
["ok"] = HttpStatusCode.OK,
["not_found"] = HttpStatusCode.NotFound,
["error"] = HttpStatusCode.InternalServerError
};
string input = "not_found";
if (statusMap.TryGetValue(input, out HttpStatusCode status))
{
Console.WriteLine(status); // NotFound
}グルーピングのパターンでは、要素をキーごとにリストへ振り分けます。
var students = new List<(string Department, string Name)>
{
("Math", "Alice"),
("Science", "Bob"),
("Math", "Charlie"),
("Science", "Diana")
};
var groups = new Dictionary<string, List<string>>();
foreach (var (dept, name) in students)
{
if (!groups.TryGetValue(dept, out List<string> list))
{
list = new List<string>();
groups[dept] = list;
}
list.Add(name);
}
foreach (var pair in groups)
{
Console.WriteLine($"{pair.Key}: {string.Join(", ", pair.Value)}");
}
// Math: Alice, Charlie
// Science: Bob, Dianaこのグルーピングのパターンは頻出するため、LINQ の GroupBy や ToLookup で代替できる場面も多くあります。ただし、ループ内で段階的に構築する必要がある場合は TryGetValue が適しています。