Why doesn't Dictionary support null key? [duplicate]
Matthew Harrington
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:
- Statically. Just call
Switch.Execute(yourObject, Switch.Case<YourType>(x => x.Action())) - 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).
010 Answers
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; }
} 4 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
5It 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.
2NameValueCollection could take null key.
1If 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(); }
} 2 NHibernate comes with a NullableDictionary. That did it for me.
1Dictionary 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.
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); } 3 EDIT: Real answer to the question actually being asked: Why can't you use null as a key for a Dictionary<bool?, 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)
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 : notnullUsage (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. :)