C# Dictionary の ContainsKey と ContainsValue - キーと値の存在確認

Dictionary に特定のキーや値が含まれているかどうかを調べたい場面はよくあります。C# の Dictionary<TKey, TValue> には、そのための専用メソッドとして ContainsKey と ContainsValue が用意されています。

ContainsKey の基本

ContainsKey は、指定したキーが Dictionary に存在するかを判定するメソッドです。戻り値は bool で、見つかれば true、なければ false を返します。

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

Console.WriteLine(scores.ContainsKey("Alice"));   // True
Console.WriteLine(scores.ContainsKey("David"));   // False

Dictionary の内部実装はハッシュテーブルなので、ContainsKey の計算量は平均 O(1) です。要素数が増えても検索速度はほぼ一定に保たれるため、大量のデータを扱う場面でも効率よく動作します。

ContainsKey の活用パターン

キーの存在を確認してから値にアクセスするのは、Dictionary を扱うときの定番パターンです。存在しないキーにインデクサでアクセスすると KeyNotFoundException がスローされるため、事前チェックが必要になります。

if (scores.ContainsKey("Alice"))
{
    int score = scores["Alice"];
    Console.WriteLine($"Alice のスコア: {score}");
}

ただし、この書き方ではキーのルックアップが 2 回発生します。ContainsKey で 1 回、インデクサで 1 回です。1 回のルックアップで存在確認と値の取得を同時に行いたい場合は、TryGetValue を使うほうが効率的です。

ContainsKey + インデクサ

存在確認と値の取得が別々の操作になり、ルックアップが 2 回発生する。コードの意図は明確だが、パフォーマンスが重要な場面では非効率になりうる。

TryGetValue

1 回のルックアップで存在確認と値の取得を同時に行える。高頻度でアクセスするホットパスでは TryGetValue が適している。

通常のアプリケーションでは 2 回のルックアップによる差はほとんど問題になりませんが、ループ内で大量に呼び出すケースでは意識しておくとよい違いです。

ContainsValue の基本

ContainsValue は、指定した値が Dictionary のいずれかのエントリに含まれているかを判定します。

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

Console.WriteLine(scores.ContainsValue(85));   // True
Console.WriteLine(scores.ContainsValue(100));  // False

ContainsKey とは異なり、ContainsValue の計算量は O(n) です。Dictionary はキーに対してハッシュテーブルを構築しますが、値にはインデックスが張られていません。そのため、すべてのエントリを先頭から順に走査して比較する必要があります。

ContainsValue の注意点

計算量が O(n) であることから、ContainsValue を頻繁に呼び出すとパフォーマンスに影響が出る可能性があります。

// 要素数が多い場合、毎回 O(n) の走査が発生する
foreach (var target in targets)
{
    if (dict.ContainsValue(target))
    {
        // ...
    }
}

このようなケースでは、値からキーを引く逆引き Dictionary を別途作成するか、値を HashSet に格納して O(1) で検索できるようにする方法が有効です。

var valueSet = new HashSet<int>(scores.Values);

Console.WriteLine(valueSet.Contains(85));   // True(O(1))
Console.WriteLine(valueSet.Contains(100));  // False(O(1))

HashSet への変換自体は O(n) のコストがかかりますが、一度作ってしまえば以降の検索はすべて O(1) になります。検索回数が多いほど効果が大きくなる手法です。

参照型の値と等価比較

ContainsValue は内部で EqualityComparer.Default を使って値を比較します。int や string などの組み込み型であればそのまま期待通りに動作しますが、独自クラスを値に使う場合は注意が必要です。

class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}

var products = new Dictionary<int, Product>
{
    [1] = new Product { Name = "Pen", Price = 100 }
};

var target = new Product { Name = "Pen", Price = 100 };
Console.WriteLine(products.ContainsValue(target)); // False

同じプロパティ値を持っていても、参照が異なるため false になります。値の等価性で判定したい場合は、Product クラスで Equals と GetHashCode をオーバーライドするか、IEquatable を実装します。

class Product : IEquatable<Product>
{
    public string Name { get; set; }
    public int Price { get; set; }

    public bool Equals(Product other)
    {
        if (other is null) return false;
        return Name == other.Name && Price == other.Price;
    }

    public override bool Equals(object obj) => Equals(obj as Product);
    public override int GetHashCode() => HashCode.Combine(Name, Price);
}

この実装を追加すれば、ContainsValue はプロパティの値に基づいて比較を行うようになります。Dictionary に限らず、LINQ の Contains や HashSet でも同様の仕組みが使われるため、独自クラスを等価比較する場面では覚えておくと役に立つ知識です。