C# TryGetValue の使い方 - 安全に値を取得する

Dictionary からキーを指定して値を取得するとき、キーが存在しなければ KeyNotFoundException が発生します。TryGetValue はこの問題を回避し、例外なしで安全に値を取得できるメソッドです。

インデクサーとの違い

インデクサーでキーを指定すると、キーが見つからない場合に例外がスローされます。

var users = new Dictionary<int, string>
{
    [1] = "Alice",
    [2] = "Bob"
};

// キーが存在する場合は問題ない
string name = users[1]; // "Alice"

// キーが存在しない場合は例外
string unknown = users[99]; // KeyNotFoundException

TryGetValue は戻り値の 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 回の検索で存在確認と値の取得を同時に行えるため、パフォーマンス上の無駄がありません。

ContainsKey + インデクサー

検索が 2 回走る。コードは読みやすいが、ホットパスでは非効率になりうる。

TryGetValue

検索が 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 が適しています。