【问题标题】:Why doesn't Dictionary<TKey, TValue> support null key? [duplicate]带有空键的字典? [复制]
【发布时间】:2011-06-05 16:47:07
【问题描述】:

首先,为什么Dictionary&lt;TKey, TValue&gt;不支持单个空键?

其次,是否存在类似字典的集合?

我想存储一个“空”或“缺失”或“默认”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&lt;YourType&gt;(x =&gt; x.Action()))
  2. 预编译。创建一个开关,稍后将其与switchInstance.Execute(yourObject) 一起使用

当您尝试将默认情况添加到“预编译”版本(空参数异常)时,效果很好except

【问题讨论】:

    标签: c#


    【解决方案1】:

    几天前我遇到了这个帖子,需要一个经过深思熟虑的巧妙解决方案来处理空键。我花时间自己实现了一个来处理更多的场景。

    您可以在我的pre-release package Teronis.NetStandard.Collections (0.1.7-alpha.37) 中找到我的 NullableKeyDictionary 实现。

    实施

    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(..) 键,您可以使用以下重载:

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

    索引器接受[AllowNull] KeyTypeNullableKey&lt;KeyType&gt;。因此,支持的场景,就像在其他帖子中说明的那样,是受支持的:

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

    我非常感谢反馈和改进建议。 :)

    【讨论】:

      【解决方案2】:

      NHibernate 带有一个 NullableDictionary。这对我有用。

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

      【讨论】:

        【解决方案3】:

        1) 为什么: 如前所述,问题在于 Dictionary 需要实现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;
            }
        }
        

        【讨论】:

        • 我刚刚知道这是可行的,因为它是一个结构。这很优雅。
        • 请注意,虽然这对引用类型有好处,但当它们使用Equals(object) 方法时,它会不必要地对值类型进行装箱/取消装箱。为避免装箱和拆箱,NullObject&lt;T&gt; 应实现 IEquatable&lt;T&gt;IEquatable&lt;NullObject&lt;T&gt;&gt;。见:medium.com/@equisept/…
        • 如果您正在寻找上述NullObject&lt;T&gt; 包装器的内置替代品,您可以使用ValueTuple&lt;T&gt; doco
        • ValueTuple 示例:var dictionary = new Dictionary, int>(); dictionary.Add(default, 1);
        【解决方案4】:

        我突然想到,您的最佳答案可能是跟踪是否定义了默认情况:

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

        你班上的其他人都不会受到影响。

        【讨论】:

        • 是的......这实际上也很有效。可能比使用虚拟类更干净。
        • 我记得有一次创建了一个 DefaultDictionary。它具有一个附加功能,即每当您尝试获取不存在的密钥时返回默认值,但除此之外应该满足需求,并且可以重复使用。不过不记得我把代码放在哪里了-.-
        【解决方案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();
            }
        }
        

        【讨论】:

        • 类名是骗人的 :) 通常 NullableX 表示 X 可以为空,不能包含空值。无论如何..我不确定什么更合适。谢谢=D
        • 我曾短暂考虑过NullableKeyDictionary,但决定将其缩写。
        【解决方案6】:

        在您的情况下,您尝试使用 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);
            }
        

        【讨论】:

        • 是的……那行得通。看起来有点难看,尤其是 other Execute 函数,但它确实有效。
        • @Ralph:您的其他 Execute 函数没有寻找 null,所以我什至没有理会它。我编辑了我的答案以包含它。请注意,由于缺少 IsAssignableFrom 用法,它的语义非常不同。
        • 嗯,不,它不需要寻找 null 因为......哦等等,是的。他们以您编写其他执行功能的方式...使行为一致,但违背了拥有 dict (快速查找)的目的。哦,好吧,无论哪种方式都无所谓...我只有 2 件物品:P
        【解决方案7】:

        编辑:对实际提出的问题的真实答案:Why can't you use null as a key for a Dictionary<bool?, string>?

        通用字典不支持 null 的原因是因为TKey 可能是一个值类型,它没有 null。

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

        要实现这一点,您可以使用非泛型 Hashtable(它使用对象键和值),也可以使用 DictionaryBase 自行开发。

        编辑:只是为了澄清为什么 null 在这种情况下是非法的,请考虑这个通用方法:

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

        但是当你打电话给IsNull&lt;int&gt;(null) 时会发生什么?

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

        您会收到编译器错误,因为您无法将 null 转换为 int。我们可以通过说我们只想要可空类型来解决它:

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

        而且,没关系。限制是我们不能再调用IsNull&lt;int&gt;,因为int不是一个类(可空对象)

        【讨论】:

        • 那是错误的。没有理由不能将值类型与null 进行比较。事实上,您的 IsNull 函数编译得很好,并为任何不可为空的值类型返回 false
        • 对不起,我的意思是IsNull&lt;T&gt;的版本没有限制。它在 C# 4 上编译得很好(并且应该在所有带有泛型的 C# 版本中做同样的事情)。
        • 显然new Dictionary&lt;int, string&gt;()[null] 是编译器错误,但new Dictionary&lt;int?, string&gt;()[null] 可以正常工作。此外,DictionaryBaseHashtable 都不允许使用 null 键!
        • @Mike: Hashtable 也不支持空键。
        • 不,字典不直接调用GetHashCode。调用失败,因为底层代码显式查找空键并拒绝它。
        【解决方案8】:

        字典会对提供的key进行hash来获取索引,如果为null,hash函数不能返回有效值,所以它不支持key中的null。

        【讨论】:

          【解决方案9】:

          它不支持它,因为字典对键进行哈希处理以确定索引,它不能对空值执行此操作。

          快速解决方法是创建一个虚拟类,并插入键值??虚拟类实例。 需要更多关于您实际尝试做的事情的信息,以减少“hacky”修复

          【讨论】:

          • 如果他们真的愿意,可以为null 创建一个特殊情况。
          • 根据我的阅读,这是不正确的。 Dictionary&lt;K,V&gt; 类使用IEqualityComparer 获取哈希码,我相信默认的只是为null 返回0。
          • 是的,它使用 IEqualityComparer,它必须自己实现 .. 你在哪里读到它返回默认值为 0 的 null?
          • Rob:Dictionary ctor 使用EqualityComparer&lt;T&gt;.Default,它调用CreateComparer(),它返回一个ObjectEqualityComparer&lt;T&gt;,其GetHashCode 函数为null 返回0。
          • 鉴于EqualityComparer 可以处理空值,Dictionary 也没有理由不能这样做。唯一可能的丑陋之处在于,如果 EqualityComparer 因为空参数而引发异常,则引发异常的参数名称可能是 xy(由 EqualityComparer 报告),而不是key
          【解决方案10】:

          NameValueCollection 可以取空键。

          【讨论】:

          • 实际上想要Dictionary&lt;Type,Action&lt;object&gt;&gt;,但这对于其他项目可能会派上用场。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-11-21
          • 1970-01-01
          • 1970-01-01
          • 2012-08-20
          • 2010-11-14
          • 1970-01-01
          相关资源
          最近更新 更多