Why doesn't Dictionary<TKey, TValue> support null key?

C#DictionaryCollections

C# Problem Overview


Firstly, why doesn't Dictionary<TKey, TValue> support a single null key?

Secondly, is there an existing dictionary-like collection that does?

I want to store an "empty" or "missing" or "default" System.Type, thought null would work well for this.


More specifically, I've written this class:

class Switch
{
    private Dictionary<Type, Action<object>> _dict;

    public Switch(params KeyValuePair<Type, Action<object>>[] cases)
    {
        _dict = new Dictionary<Type, Action<object>>(cases.Length);
        foreach (var entry in cases)
            _dict.Add(entry.Key, entry.Value);
    }

    public void Execute(object obj)
    {
        var type = obj.GetType();
        if (_dict.ContainsKey(type))
            _dict[type](obj);
    }

    public static void Execute(object obj, params KeyValuePair<Type, Action<object>>[] cases)
    {
        var type = obj.GetType();
      
        foreach (var entry in cases)
        {
            if (entry.Key == null || type.IsAssignableFrom(entry.Key))
            {
                entry.Value(obj);
                break;
            }
        }
    }

    public static KeyValuePair<Type, Action<object>> Case<T>(Action action)
    {
        return new KeyValuePair<Type, Action<object>>(typeof(T), x => action());
    }

    public static KeyValuePair<Type, Action<object>> Case<T>(Action<T> action)
    {
        return new KeyValuePair<Type, Action<object>>(typeof(T), x => action((T)x));
    }

    public static KeyValuePair<Type, Action<object>> Default(Action action)
    {
        return new KeyValuePair<Type, Action<object>>(null, x => action());
    }
}

For switching on types. There are two ways to use it:

  1. Statically. Just call Switch.Execute(yourObject, Switch.Case<YourType>(x => x.Action()))
  2. Precompiled. Create a switch, and then use it later with switchInstance.Execute(yourObject)

Works great except when you try to add a default case to the "precompiled" version (null argument exception).

C# Solutions


Solution 1 - C#

  1. Why: As described before, the problem is that Dictionary requires an implementation of the Object.GetHashCode() method. null does not have an implementation, therefore no hash code associated.

  2. Solution: I have used a solution similar to a NullObject pattern using generics that enables you to use the dictionary seamlessly (no need for a different dictionary implementation).

You can will use it, like this:

var dict = new Dictionary<NullObject<Type>, string>();
dict[typeof(int)] = "int type";
dict[typeof(string)] = "string type";
dict[null] = "null type";

Assert.AreEqual("int type", dict[typeof(int)]);
Assert.AreEqual("string type", dict[typeof(string)]);
Assert.AreEqual("null type", dict[null]);

You just need to create this struct once in a lifetime :

public struct NullObject<T>
{
    [DefaultValue(true)]
    private bool isnull;// default property initializers are not supported for structs

    private NullObject(T item, bool isnull) : this()
    {
        this.isnull = isnull;
        this.Item = item;
    }
      
    public NullObject(T item) : this(item, item == null)
    {
    }

    public static NullObject<T> Null()
    {
        return new NullObject<T>();
    }

    public T Item { get; private set; }

    public bool IsNull()
    {
        return this.isnull;
    }

    public static implicit operator T(NullObject<T> nullObject)
    {
        return nullObject.Item;
    }

    public static implicit operator NullObject<T>(T item)
    {
        return new NullObject<T>(item);
    }

    public override string ToString()
    {
        return (Item != null) ? Item.ToString() : "NULL";
    }

    public override bool Equals(object obj)
    {
        if (obj == null)
            return this.IsNull();

        if (!(obj is NullObject<T>))
            return false;

        var no = (NullObject<T>)obj;

        if (this.IsNull())
            return no.IsNull();

        if (no.IsNull())
            return false;

        return this.Item.Equals(no.Item);
    }

    public override int GetHashCode()
    {
        if (this.isnull)
            return 0;

        var result = Item.GetHashCode();

        if (result >= 0)
            result++;

        return result;
    }
}

Solution 2 - C#

It doesn't support it because the dictionary hashes the key to determine the index, which it can't do on a null value.

A quick fix would be to create a dummy class, and insert the key value ?? dummyClassInstance. Would need more information about what you're actually trying to do to give a less 'hacky' fix

Solution 3 - C#

It just hit me that your best answer is probably to just keep track of whether a default case has been defined:

class Switch
{
    private Dictionary<Type, Action<object>> _dict;
    private Action<object> defaultCase;

    public Switch(params KeyValuePair<Type, Action<object>>[] cases)
    {
        _dict = new Dictionary<Type, Action<object>>(cases.Length);
        foreach (var entry in cases)
            if (entry.Key == null)
                defaultCase = entry.Value;
            else
                _dict.Add(entry.Key, entry.Value);
    }

    public void Execute(object obj)
    {
        var type = obj.GetType();
        if (_dict.ContainsKey(type))
            _dict[type](obj);
        else if (defaultCase != null)
            defaultCase(obj);
    }

...

The whole rest of your class would remain untouched.

Solution 4 - C#

NameValueCollection could take null key.

Solution 5 - C#

If you really want a dictionary that allows null keys, here's my quick implementation (not well-written or well-tested):

class NullableDict<K, V> : IDictionary<K, V>
{
    Dictionary<K, V> dict = new Dictionary<K, V>();
    V nullValue = default(V);
    bool hasNull = false;

    public NullableDict()
    {
    }

    public void Add(K key, V value)
    {
        if (key == null)
            if (hasNull)
                throw new ArgumentException("Duplicate key");
            else
            {
                nullValue = value;
                hasNull = true;
            }
        else
            dict.Add(key, value);
    }

    public bool ContainsKey(K key)
    {
        if (key == null)
            return hasNull;
        return dict.ContainsKey(key);
    }

    public ICollection<K> Keys
    {
        get 
        {
            if (!hasNull)
                return dict.Keys;

            List<K> keys = dict.Keys.ToList();
            keys.Add(default(K));
            return new ReadOnlyCollection<K>(keys);
        }
    }

    public bool Remove(K key)
    {
        if (key != null)
            return dict.Remove(key);

        bool oldHasNull = hasNull;
        hasNull = false;
        return oldHasNull;
    }

    public bool TryGetValue(K key, out V value)
    {
        if (key != null)
            return dict.TryGetValue(key, out value);

        value = hasNull ? nullValue : default(V);
        return hasNull;
    }

    public ICollection<V> Values
    {
        get
        {
            if (!hasNull)
                return dict.Values;

            List<V> values = dict.Values.ToList();
            values.Add(nullValue);
            return new ReadOnlyCollection<V>(values);
        }
    }

    public V this[K key]
    {
        get
        {
            if (key == null)
                if (hasNull)
                    return nullValue;
                else
                    throw new KeyNotFoundException();
            else
                return dict[key];
        }
        set
        {
            if (key == null)
            {
                nullValue = value;
                hasNull = true;
            }
            else
                dict[key] = value;
        }
    }

    public void Add(KeyValuePair<K, V> item)
    {
        Add(item.Key, item.Value);
    }

    public void Clear()
    {
        hasNull = false;
        dict.Clear();
    }

    public bool Contains(KeyValuePair<K, V> item)
    {
        if (item.Key != null)
            return ((ICollection<KeyValuePair<K, V>>)dict).Contains(item);
        if (hasNull)
            return EqualityComparer<V>.Default.Equals(nullValue, item.Value);
        return false;
    }

    public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex)
    {
        ((ICollection<KeyValuePair<K, V>>)dict).CopyTo(array, arrayIndex);
        if (hasNull)
            array[arrayIndex + dict.Count] = new KeyValuePair<K, V>(default(K), nullValue);
    }

    public int Count
    {
        get { return dict.Count + (hasNull ? 1 : 0); }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(KeyValuePair<K, V> item)
    {
        V value;
        if (TryGetValue(item.Key, out value) && EqualityComparer<V>.Default.Equals(item.Value, value))
            return Remove(item.Key);
        return false;
    }

    public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
    {
        if (!hasNull)
            return dict.GetEnumerator();
        else
            return GetEnumeratorWithNull();
    }

    private IEnumerator<KeyValuePair<K, V>> GetEnumeratorWithNull()
    {
        yield return new KeyValuePair<K, V>(default(K), nullValue);
        foreach (var kv in dict)
            yield return kv;
    }

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

Solution 6 - C#

Solution 7 - C#

Dictionary will hash the key supplie to get the index , in case of null , hash function can not return a valid value that's why it does not support null in key.

Solution 8 - C#

In your case you are trying to use null as a sentinel value (a "default") instead of actually needing to store null as a value. Rather than go to the hassle of creating a dictionary that can accept null keys, why not just create your own sentinel value. This is a variation on the "null object pattern":

class Switch
{
    private class DefaultClass { }

    ....

    public void Execute(object obj)
    {
        var type = obj.GetType();
        Action<object> value;
        // first look for actual type
        if (_dict.TryGetValue(type, out value) ||
        // look for default
            _dict.TryGetValue(typeof(DefaultClass), out value))
            value(obj);
    }

    public static void Execute(object obj, params KeyValuePair<Type, Action<object>>[] cases)
    {
        var type = obj.GetType();
      
        foreach (var entry in cases)
        {
            if (entry.Key == typeof(DefaultClass) || type.IsAssignableFrom(entry.Key))
            {
                entry.Value(obj);
                break;
            }
        }
    }

    ...

    public static KeyValuePair<Type, Action<object>> Default(Action action)
    {
        return new KeyValuePair<Type, Action<object>>(new DefaultClass(), x => action());
    }
}

Note that your first Execute function differs significantly from your second. It may be the case that you want something like this:

    public void Execute(object obj)
    {
        Execute(obj, (IEnumerable<KeyValuePair<Type, Action<object>>>)_dict);
    }

    public static void Execute(object obj, params KeyValuePair<Type, Action<object>>[] cases)
    {
        Execute(obj, (IEnumerable<KeyValuePair<Type, Action<object>>>)cases);
    }

    public static void Execute(object obj, IEnumerable<KeyValuePair<Type, Action<object>>> cases)
    {
        var type = obj.GetType();
        Action<object> defaultEntry = null;
        foreach (var entry in cases)
        {
            if (entry.Key == typeof(DefaultClass))
                defaultEntry = entry.Value;
            if (type.IsAssignableFrom(entry.Key))
            {
                entry.Value(obj);
                return;
            }
        }
        if (defaultEntry != null)
            defaultEntry(obj);
    }

Solution 9 - C#

EDIT: Real answer to the question actually being asked: https://stackoverflow.com/questions/2174692/why-cant-you-use-null-as-a-key-for-a-dictionarybool-string

The reason the generic dictionary doesn't support null is because TKey might be a value type, which doesn't have null.

new Dictionary<int, string>[null] = "Null"; //error!

To get one that does, you could either use the non-generic Hashtable (which uses object keys and values), or roll your own with DictionaryBase.

Edit: just to clarify why null is illegal in this case, consider this generic method:

bool IsNull<T> (T value) {
    return value == null;
}

But what happens when you call IsNull<int>(null)?

Argument '1': cannot convert from '<null>' to 'int'

You get a compiler error, since you can't convert null to an int. We can fix it, by saying that we only want nullable types:

bool IsNull<T> (T value) where T : class {
    return value == null;
}

And, that's A-Okay. The restriction is that we can no longer call IsNull<int>, since int is not a class (nullable object)

Solution 10 - C#

I come across this thread some days ago and needed a well thought out and clever solution to handle null keys. I took the time and implemented one by me to handle more scenarios.

You can find my implementation of NullableKeyDictionary currently in my pre-release package Teronis.NetStandard.Collections (0.1.7-alpha.37).

Implementation

public class NullableKeyDictionary<KeyType, ValueType> : INullableKeyDictionary<KeyType, ValueType>, IReadOnlyNullableKeyDictionary<KeyType, ValueType>, IReadOnlyCollection<KeyValuePair<INullableKey<KeyType>, ValueType>> where KeyType : notnull

public interface INullableKeyDictionary<KeyType, ValueType> : IDictionary<KeyType, ValueType>, IDictionary<NullableKey<KeyType>, ValueType> where KeyType : notnull

public interface IReadOnlyNullableKeyDictionary<KeyType, ValueType> : IReadOnlyDictionary<KeyType, ValueType>, IReadOnlyDictionary<NullableKey<KeyType>, ValueType> where KeyType : notnull

Usage (Excerpt of the Xunit test)

// Assign.
var dictionary = new NullableKeyDictionary<string, string>();
IDictionary<string, string> nonNullableDictionary = dictionary;
INullableKeyDictionary<string, string> nullableDictionary = dictionary;

// Assert.
dictionary.Add("value");
/// Assert.Empty does cast to IEnumerable, but our implementation of IEnumerable 
/// returns an enumerator of type <see cref="KeyValuePair{NullableKey, TValue}"/>.
/// So we test on correct enumerator implementation wether it can move or not.
Assert.False(nonNullableDictionary.GetEnumerator().MoveNext());
Assert.NotEmpty(nullableDictionary);
Assert.Throws<ArgumentException>(() => dictionary.Add("value"));

Assert.True(dictionary.Remove());
Assert.Empty(nullableDictionary);

dictionary.Add("key", "value");
Assert.True(nonNullableDictionary.GetEnumerator().MoveNext());
Assert.NotEmpty(nullableDictionary);
Assert.Throws<ArgumentException>(() => dictionary.Add("key", "value"));

dictionary.Add("value");
Assert.Equal(1, nonNullableDictionary.Count);
Assert.Equal(2, nullableDictionary.Count);

The following overloads exists for Add(..):

void Add([AllowNull] KeyType key, ValueType value)
void Add(NullableKey<KeyType> key, [AllowNull] ValueType value)
void Add([AllowNull] ValueType value); // Shortcut for adding value with null key.

This class should behave same and intuitive as the dictionary does.

For Remove(..) keys you can use the following overloads:

void Remove([AllowNull] KeyType key)
void Remove(NullableKey<KeyType> key)
void Remove(); // Shortcut for removing value with null key.

The indexers do accept [AllowNull] KeyType or NullableKey<KeyType>. So supported scenarios, like they are stated in other posts, are supported:

var dict = new NullableKeyDictionary<Type, string>
dict[typeof(int)] = "int type";
dict[typeof(string)] = "string type";

dict[null] = "null type";
// Or:
dict[NullableKey<Type>.Null] = "null type";

I highly appreciate feedback and suggestions for improvements. :)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmpenView Question on Stackoverflow
Solution 1 - C#Fabio MarrecoView Answer on Stackoverflow
Solution 2 - C#RobView Answer on Stackoverflow
Solution 3 - C#GabeView Answer on Stackoverflow
Solution 4 - C#Darin DimitrovView Answer on Stackoverflow
Solution 5 - C#GabeView Answer on Stackoverflow
Solution 6 - C#TobiasView Answer on Stackoverflow
Solution 7 - C#TalentTunerView Answer on Stackoverflow
Solution 8 - C#GabeView Answer on Stackoverflow
Solution 9 - C#Mike CaronView Answer on Stackoverflow
Solution 10 - C#TeronekoView Answer on Stackoverflow