Словарь с нулевым ключом?
Во-первых, почему 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.
Вам понадобится дополнительная информация о том, что вы на самом деле пытаетесь сделать, чтобы дать менее "хакерское" исправление.
Ответ 4
NameValueCollection может принимать нулевой ключ.
Ответ 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();
}
}
Ответ 6
NHibernate поставляется с NullableDictionary. Это сделало это для меня.
https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Util/NullableDictionary.cs
Ответ 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 хеш-функция не может вернуть допустимое значение, поэтому он не поддерживает нуль в ключе.