68

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).

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
mpen
  • 272,448
  • 266
  • 850
  • 1,236

10 Answers10

42
  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 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;
    }
}
Fabio Marreco
  • 2,186
  • 2
  • 23
  • 24
  • I just got that this works because its a struct. That's pretty elegant. – JonnyRaa Apr 30 '15 at 14:59
  • 4
    Do note that while this is good for reference types, it will unnecessarily box/unbox for value types when they use the `Equals(object)` method. To avoid boxing and unboxing, `NullObject` should implement `IEquatable` and `IEquatable>`. See: https://medium.com/@equisept/c-journey-into-struct-equality-comparison-deep-dive-9693f74562f1 – NightOwl888 Nov 12 '19 at 02:57
  • 9
    If you are looking for a built-in alternative to the `NullObject` wrapper above, you can use `ValueTuple` [doco](https://learn.microsoft.com/en-us/dotnet/api/system.valuetuple-1?view=net-5.0) – Matt Quail Apr 08 '21 at 08:16
  • 2
    ValueTuple example: var dictionary = new Dictionary, int>(); dictionary.Add(default, 1); – Edward Olamisan Apr 21 '21 at 14:53
14

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

Rob
  • 26,989
  • 16
  • 82
  • 98
  • 10
    Could create a special case for `null` if they really wanted to. – mpen Jan 08 '11 at 07:51
  • 3
    That's not correct based on my reading. The `Dictionary` class uses `IEqualityComparer` to get the hash code, and I believe that the default one just returns 0 for `null`. – Gabe Jan 08 '11 at 07:52
  • Yes, it uses the IEqualityComparer, which it has to implement itself .. where did you read it returns a default of 0 for null? – Rob Jan 08 '11 at 08:04
  • 11
    Rob: The `Dictionary` ctor uses `EqualityComparer.Default`, which calls `CreateComparer()`, which returns an `ObjectEqualityComparer`, whose `GetHashCode` function returns 0 for `null`. – Gabe Jan 08 '11 at 08:25
  • 3
    Given an `EqualityComparer` that can handle null values, there's no reason a `Dictionary` couldn't do so as well. The only potential ugliness would be that if an `EqualityComparer` were to throw an exception because of a null argument, the parameter name of the thrown exception would probably be `x` or `y` (as reported by the `EqualityComparer`), rather than `key`. – supercat Dec 30 '13 at 18:23
14

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.

Gabe
  • 84,912
  • 12
  • 139
  • 238
  • Yeah... this would work pretty well too actually. Might be a bit cleaner than using a dummy class. – mpen Jan 09 '11 at 17:57
  • I remember creating a DefaultDictionary at one time. It had the added feature of returning the Default whenever you tried to get a key that didn't exist, but other than that should fit the needs, and be plenty reusable. Don't remember where I put the code though -.- – Alxandr Jan 27 '13 at 23:23
9

NameValueCollection could take null key.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
5

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();
    }
}
Gabe
  • 84,912
  • 12
  • 139
  • 238
  • The name of the class is deceiving :) Usually NullableX means X can be null, not contain nulls. No matter though.. I'm not sure what would be more appropriate anyway. Thanks =D – mpen Jan 09 '11 at 17:59
  • 1
    I briefly considered `NullableKeyDictionary` but decided to abbreviate it. – Gabe Jan 09 '11 at 18:56
3

NHibernate comes with a NullableDictionary. That did it for me.

https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Util/NullableDictionary.cs

Tobias
  • 2,945
  • 5
  • 41
  • 59
  • 1
    That `NullableDictionary` implementation is flawed. If I pass a custom `IEqualityComparer` that equates `null` with `MyObject.Null`, then add the key `null` and then ask if the key `MyObject.Null` exists, this implementation will reply that the key does not exist, although it does. It's like using a `StringComparer.OrdinalIgnoreCase` and adding the key `"hello"` and asking if the key `"HELLO"` exists. The correct response is `true`, because based on this comparer `"hello"` and `"HELLO"` are identical. – Theodor Zoulias Feb 24 '22 at 15:57
1

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.

TalentTuner
  • 17,262
  • 5
  • 38
  • 63
1

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);
    }
Gabe
  • 84,912
  • 12
  • 139
  • 238
  • Yup... that'll work. Seems a bit uglier, esp for the *other* Execute function, but it does work. – mpen Jan 08 '11 at 08:38
  • @Ralph: Your other Execute function didn't look for `null` so I didn't even bother with it. I edited my answer to include it. Note that it has very different semantics due to its lack of `IsAssignableFrom` usage. – Gabe Jan 08 '11 at 08:50
  • Well no, it wouldn't need to look for null because... oh wait, yeah it would. They way you've written the other execute function... makes the behaviour consistent, but defeats the purpose of having a dict (quick lookups). Oh well, doesn't really matter either way... I've only got 2 items in it :P – mpen Jan 09 '11 at 17:55
0

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. :)

Teneko
  • 1,417
  • 9
  • 16
-1

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)

Community
  • 1
  • 1
Mike Caron
  • 14,351
  • 4
  • 49
  • 77
  • I don't see this non-generic Dictionary on MSDN. You have a link? – mpen Jan 08 '11 at 07:54
  • 6
    That's just false. There's no reason you can't compare a value type to `null`. In fact, your `IsNull` function compiles just fine and returns `false` for any non-nullable value types. – Gabe Jan 08 '11 at 07:56
  • Sorry, my bad. It's called `Hashtable`. I've updated my answer to reflect that. – Mike Caron Jan 08 '11 at 07:56
  • @Gabe: Have you tried calling it? `Argument '1': cannot convert from '' to 'int'` – Mike Caron Jan 08 '11 at 07:58
  • @Gabe: However, you're correct. I misplaced where the error lies, and have updated my answer. – Mike Caron Jan 08 '11 at 08:01
  • 1
    Sorry, I meant the version of `IsNull` without constraints. It compiles just fine on C# 4 (and should do the same in all versions of C# with generics). – Gabe Jan 08 '11 at 08:01
  • 1
    Obviously `new Dictionary()[null]` is a compiler error, but `new Dictionary()[null]` could work fine. Furthermore, neither `DictionaryBase` nor `Hashtable` allow `null` keys! – Gabe Jan 08 '11 at 08:05
  • 3
    @Mike: Hashtable doesn't support null keys either. – mpen Jan 08 '11 at 08:05
  • @Gabe: Yes, the method itself compiles. It was the call that bombs. However, I was under the impression that the OP had a compiler error, not an exception. My bad. It probably rejects nulls because it calls `GetHashCode()`, but I don't know for sure... – Mike Caron Jan 08 '11 at 08:10
  • Anyway, turns out, this question has been asked before, so I've added the link to the answer in my answer. – Mike Caron Jan 08 '11 at 08:15
  • 3
    No, the Dictionary doesn't directly call `GetHashCode`. The call fails because the underlying code explicitly looks for a null key and rejects it. – Gabe Jan 08 '11 at 08:19