Why can't you use null as a key for a Dictionary<bool?, string>?

Apparently, you cannot use a null for a key, even if your key is a nullable type.

This code:

var nullableBoolLabels = new System.Collections.Generic.Dictionary<bool?, string>
{
    { true, "Yes" },
    { false, "No" },
    { null, "(n/a)" }
};

...results in this exception:

Value cannot be null. Parameter name: key

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

[ArgumentNullException: Value cannot be null. Parameter name: key] System.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument) +44 System.Collections.Generic.Dictionary'2.Insert(TKey key, TValue value, Boolean add) +40
System.Collections.Generic.Dictionary'2.Add(TKey key, TValue value) +13

Why would the .NET framework allow a nullable type for a key, but not allow a null value?


Solution 1:

It would tell you the same thing if you had a Dictionary<SomeType, string>, SomeType being a reference type, and you tried to pass null as the key, it is not something affecting only nullable type like bool?. You can use any type as the key, nullable or not.

It all comes down to the fact that you can't really compare nulls. I assume the logic behind not being able to put null in the key, a property that is designed to be compared with other objects is that it makes it incoherent to compare null references.

If you want a reason from the specs, it boils down to a "A key cannot be a null reference " on MSDN.

If you want an exemple of a possible workaround, you can try something similar to Need an IDictionary implementation that will allow a null key

Solution 2:

Oftentimes you have to go back to C++ methodologies and techniques to fully understand how and why the .NET Framework works in a particular manner.

In C++, you oftentimes have to pick a key that will not be used - the dictionary uses this key to point to deleted and/or empty entries. For instance, you have a dictionary of <int, int>, and after inserting an entry, you delete it. Rather than running the Garbage Cleanup right then and there, restructuring the dictionary, and leading to bad performance; the dictionary will just replace the KEY value with the key you previously selected, basically meaning "when you're traversing the dictionary memoryspace, pretend this <key,value> pair does not exist, feel free to overwrite it."

Such a key is also used in dictionaries that pre-allocate space in buckets in a particular manner - you need a key to "initialize" the buckets with instead of having a flag for each entry that indicates whether or not its contents are valid. So instead of having a triple <key, value, initialized> you would have a tuple <key, value> with the rule being that if key == empty_key then it hasn't been initialized - and therefore you may not use empty_key as a valid KEY value.

You can see this sort of behavior in the Google hashtable (dictionary for you .NET people :) in the documentation here: http://google-sparsehash.googlecode.com/svn/trunk/doc/dense_hash_map.html

Look at the set_deleted_key and set_empty_key functions to get what I'm talking about.

I'd wager .NET uses NULL as either the unique deleted_key or empty_key in order to do these sort of nifty tricks that improve performance.

Solution 3:

You can't use a null bool? because nullable types are meant to act like reference types. You can't use a null reference as a dictionary key, either.

The reason you can't use a null reference as a dictionary key probably comes down to a design decision at Microsoft. Allowing null keys requires checking for them, which makes the implementation slower and more complicated. For example, the implementation would have to avoid using .Equals or .GetHashCode on a null reference.

I agree that allowing null keys would be preferable, but it's too late to change the behavior now. If you need a workaround you can write your own dictionary with allowed null keys, or you could write a wrapper struct which implicitly converts to/from T and make that the key-type for your dictionary (ie. the struct would wrap the null and handle comparing and hashing, so the dictionary never 'sees' the null).

Solution 4:

There is no fundamental reason. HashSet allows null and a HashSet is simply a Dictionary where the key is the same type as the value. So, really, null keys should have been allowed but would be breaking to change it now so we're stuck with it.