Dictionary<TKey, TValue>はキーとしてnullを許容しない


Dictionary<TKey, TValue>の公式ドキュメント

公式ドキュメントの注釈より

A key cannot be null, but a value can be, if its type TValue is a reference type.

Dictionary<TKey, TValue>において、キーとしてnullは使えません。(参照型の場合。値型はそもそもnullにできない。)

を使う際、nullをキーとして呼び出すと、ArgumentNullExceptionが投げられます。

これは、それらのメソッドの内部でキーのGetHashCodeメソッドを呼び出す必要があるためです。

次のようなコレクション初期化子は内部で、Addメソッドを使っているので、コレクション初期化子でも例外が発生することに注意してください。

// 例外が発生する
var dict = new Dictionary<string, int> {
   { "a", 0 },
   { "b", 0 },
   { "c", 0 },
   { null, 0 },    
};

さて、これは実装である「Dictionary<TKey, TValue>」の仕様です。

のリファレンスには、

Implementations can vary in whether they allow you to specify a key that is null.

とあります。

キーとしてnullを許可するかどうかは、IDictionary<TKey, TValue>IReadOnlyDictionary<TKey, TValue>を実装するクラスのその実装に任されています。

キーとしてnullを許容するIReadOnlyDictionary<TKey, TValue>を実装するクラスの例を次に示します。

public class NullableKeyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
    private readonly IReadOnlyDictionary<TKey, TValue> source;
    private readonly bool hasNullKey;
    private readonly TValue nullValue;

    public NullableKeyDictionary(IReadOnlyDictionary<TKey, TValue> source)
    {
        if (this.source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        this.source = source;
        this.hasNullKey = false;
        this.nullValue = default(TValue);
    }

    public NullableKeyDictionary(IReadOnlyDictionary<TKey, TValue> source, TValue nullValue)
    {
        if (this.source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        this.source = source;
        this.hasNullKey = true;
        this.nullValue = nullValue;
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        foreach (var it in this.source)
        {
            yield return it;
        }

        if (this.hasNullKey)
        {
            yield return new KeyValuePair<TKey, TValue>(default(TKey), nullValue);
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public int Count => this.hasNullKey ? this.source.Count + 1 : this.source.Count;

    public bool ContainsKey(TKey key)
    {
        if (this.hasNullKey && key == null)
        {
            return true;
        }
        else
        {
            return this.source.ContainsKey(key);
        }
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        if (this.hasNullKey && key == null)
        {
            value = nullValue;
            return true;
        }
        else
        {
            return this.TryGetValue(key, out value);
        }
    }

    public TValue this[TKey key] => key == null && hasNullKey
        ? this.nullValue
        : this.source[key];

    public IEnumerable<TKey> Keys
    {
        get
        {
            foreach (var it in this.source)
            {
                yield return it.Key;
            }

            if (this.hasNullKey)
            {
                yield return default(TKey);
            }
        }
    }

    public IEnumerable<TValue> Values
    {
        get
        {
            foreach (var it in this.source)
            {
                yield return it.Value;
            }

            if (this.hasNullKey)
            {
                yield return nullValue;
            }
        }
    }
}