Словарь с нулевым ключом?

Во-первых, почему Dictionary<TKey, TValue> не поддерживает один пустой ключ?

Во-вторых, существует ли существующая коллекция, подобная словарю?

Я хочу сохранить "пустой" или "отсутствующий" или "по умолчанию" System.Type, думал, что null будет хорошо работать для этого.


В частности, я написал этот класс:

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());
    }
}

Для включения типов. Существует два способа его использования:

  • Статически. Просто позвоните Switch.Execute(yourObject, Switch.Case<YourType>(x => x.Action()))
  • прекомпилирована. Создайте переключатель, а затем используйте его позже switchInstance.Execute(yourObject)

Работает отлично, если вы не попытаетесь добавить случай по умолчанию в "предварительно скомпилированную" версию (исключение из нулевого аргумента).

Ответ 1

Это просто поразило меня тем, что лучший ответ - это просто отслеживать, был ли задан случай по умолчанию:

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);
    }

...

Весь остальной класс будет оставаться нетронутым.

Ответ 2

1) Почему: Как описано выше, проблема в том, что словарь требует реализации метода Object.GetHashCode(). null не имеет реализации, поэтому не связан хэш-код.

2) Решение. Я использовал решение, подобное шаблону NullObject, с использованием дженериков, который позволяет вам легко использовать словарь (нет необходимости в другой реализации словаря).

Вы можете использовать его, например:

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]);

Вам просто нужно создать эту структуру один раз в жизни:

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;
    }
}

Ответ 3

Он не поддерживает его, потому что словарь хэширует ключ, чтобы определить индекс, который он не может сделать с нулевым значением.

Быстрое исправление заключалось бы в создании фиктивного класса и введении значения ключа? dummyClassInstance. Вам понадобится дополнительная информация о том, что вы на самом деле пытаетесь сделать, чтобы дать менее "хакерское" исправление.

Ответ 5

Если вам действительно нужен словарь, который разрешает нулевые ключи, здесь моя быстрая реализация (не хорошо написана или хорошо протестирована):

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();
    }
}

Ответ 7

В вашем случае вы пытаетесь использовать null в качестве значения отправителя ( "по умолчанию" ) вместо фактического хранения null в качестве значения. Вместо того, чтобы переходить к проблеме создания словаря, который может принимать нулевые ключи, почему бы просто не создать собственное значение дозорного. Это вариация "шаблона нулевого объекта":

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());
    }
}

Обратите внимание, что ваша первая функция Execute значительно отличается от вашей второй. Возможно, вам нужно что-то вроде этого:

    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);
    }

Ответ 8

EDIT: реальный ответ на заданный вопрос: Почему вы не можете использовать null в качестве ключа для словаря < bool?, string > ?

Причина, по которой общий словарь не поддерживает null, состоит в том, что TKey может быть типом значения, который не имеет значения null.

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

Чтобы получить тот, который делает, вы можете либо использовать не-общий Hashtable (который использует объектные ключи и значения), либо сворачивать свой собственный с помощью DictionaryBase.

Изменить: просто чтобы выяснить, почему null является незаконным в этом случае, рассмотрите этот общий метод:

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

Но что происходит, когда вы вызываете IsNull<int>(null)?

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

Вы получаете ошибку компилятора, так как вы не можете преобразовать null в int. Мы можем исправить это, сказав, что нам нужны только нулевые типы:

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

И, что-ладно. Ограничение состоит в том, что мы больше не можем вызывать IsNull<int>, так как int не является классом (объект с нулевым значением)

Ответ 9

Словарь будет хешировать ключ supplie для получения индекса, в случае null хеш-функция не может вернуть допустимое значение, поэтому он не поддерживает нуль в ключе.