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

Во-первых, почему не 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());
    }
}

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

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

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

9 ответов

Решение

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

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

...

Вся остальная часть вашего класса останется нетронутой.

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

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

Быстрое решение проблемы - создать фиктивный класс и вставить значение ключа. dummyClassInstance. Потребовалось бы больше информации о том, что вы на самом деле пытаетесь сделать, чтобы дать менее "хакерское" исправление

NameValueCollection может принимать нулевой ключ.

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

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

NHibernate поставляется с NullableDictionary. Это сделало это для меня.

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

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

В вашем случае вы пытаетесь использовать 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);
    }

РЕДАКТИРОВАТЬ: Реальный ответ на вопрос, который на самом деле задают: почему вы не можете использовать ноль в качестве ключа для словаря?

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

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 это не класс (обнуляемый объект)

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

Вы можете найти мою реализацию NullableKeyDictionary на данный момент в моем https://www.nuget.org/packages/Teronis.NetStandard.Collections/0.1.7-alpha.37.

Реализация

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

Использование (отрывок из теста Xunit)

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

Следующие перегрузки существуют для 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.

Этот класс должен вести себя так же интуитивно, как словарь.

За Remove(..) keys вы можете использовать следующие перегрузки:

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

Индексаторы принимают [AllowNull] KeyType или NullableKey<KeyType>. Итак, поддерживаются поддерживаемые сценарии, как они изложены в других сообщениях:

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

Я высоко ценю отзывы и предложения по улучшению.:)

Другие вопросы по тегам