为啥不能使用 null 作为 Dictionary<bool?, string> 的键?

Posted

技术标签:

【中文标题】为啥不能使用 null 作为 Dictionary<bool?, string> 的键?【英文标题】:Why can't you use null as a key for a Dictionary<bool?, string>?为什么不能使用 null 作为 Dictionary<bool?, string> 的键? 【发布时间】:2011-01-11 14:15:37 【问题描述】:

显然,您不能将 null 用作键,即使您的键是可为空的类型。

这段代码:

var nullableBoolLabels = new System.Collections.Generic.Dictionary<bool?, string>

     true, "Yes" ,
     false, "No" ,
     null, "(n/a)" 
;

...导致此异常:

值不能为空。 参数名称:key

说明:在执行当前 Web 请求期间发生未处理的异常。请查看堆栈跟踪以获取有关错误及其源自代码的位置的更多信息。

[ArgumentNullException: Value cannot be null. Parameter name: key] System.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument) +44 System.Collections.Generic.Dictionary'2.Insert(TKey key, TValue value, Boolean add) +40System.Collections.Generic.Dictionary'2.Add(TKey key, TValue value) +13

为什么 .NET 框架允许键为空类型,但不允许空值?

【问题讨论】:

为什么你需要一本字典呢? switch 语句完成同样的工作:string getLabel(bool? value) if (value == null) ... else if ... else ... ; @Juliet,说来话长,但我会尝试:我将在三个不同的地方使用三个标签(是,否,n/a):(1)bool? -&gt; label转换器,(2) 一个label -&gt; bool? 转换器,和(3) 一个SelectList 用于DropDownMenu(我只是将字典值传递给它)。根据 DRY,我希望将标签放在一个位置,以防我后来改变主意,比如 Y、N、NA。由于这是不允许的,我最终选择了三个常量(每个标签一个)和一个字符串数组用于SelectList。不太方便,但已经足够了。 有时你必须回到 C++ 才能看到 .NET 中实现背后的逻辑。看看下面我的回答。 @Juliet,在我的情况下,我需要一个 Guid? 作为键。您可以将bool? 视为最简单的可用示例。 这是一个很好的问题,特别是因为 HashSet 可以很好地处理空条目。 【参考方案1】:

没有根本原因。 HashSet 允许 null 并且 HashSet 只是一个字典,其中键与值的类型相同。所以,实际上,应该允许空键,但现在要更改它会破坏它,所以我们坚持使用它。

【讨论】:

【参考方案2】:

我刚刚阅读了这篇文章;正如 Eric 回答的那样,我现在认为这是不正确的,并非所有字符串都自动被 Interned,我需要覆盖相等操作。


当我将字典从使用字符串作为键转换为字节数组时,这让我大吃一惊。

我在 vanilla C 的思维模式中,字符串只是一个字符数组,所以我花了一段时间才弄清楚为什么通过连接构建的字符串作为查找的键,而在循环中构建的字节数组确实如此不是。

这是因为 .net 在内部将所有包含相同值的字符串分配给相同的引用。 (这叫“实习”)

所以,运行后:


string str1 = "AB";
string str2 = "A";
str1 += "C";
str2 += "BC";

str1 和 str2 实际上指向内存中完全相同的位置!这使它们成为相同的对象;它允许字典使用 str2 查找以 str1 作为键添加的项目。

如果你:


char[3] char1;
char[3] char2;
char1[0] = 'A';
char1[1] = 'B';
char1[2] = 'C';
char2[0] = 'A';
char2[1] = 'B';
char2[2] = 'C';

char1 和 char2 是不同的引用;如果您使用 char1 将项目添加到字典中,则无法使用 char2 进行查找。

【讨论】:

不,这是错误的。如果字符串是计算的结果,则它们不会被保留。您的问题是 byte[] 不会覆盖 GetHashCode 和 Equals,因此您可以获得参考比较,但 string 可以提供值比较。 当然这就是您所期望的!仅仅因为两个对象具有相同的值并不能使它们成为同一个对象。在 c# 中,字符串是不可变的,所以所有操作实际上都会给你一个新字符串【参考方案3】:

字典(基本说明) 字典是 Hashtable 类的通用(类型化)实现,在 .NET 框架 2.0 中引入。

哈希表存储基于键(更具体地说是键的哈希)的值。 .NET 中的每个对象都有 GetHashCode 方法。 当您将键值对插入哈希表时,会在该键上调用 GetHashCode。 想一想:你不能在null 上调用GetHashCode 方法。

那么 Nullable 类型呢?Nullable 类只是一个包装器,允许将空值分配给值类型。基本上,包装器由一个 HasValue 布尔值组成,它告诉它是否为空,以及一个 Value 包含值类型的值。

把它们放在一起,你会得到什么 .NET 并不真正关心您使用什么作为哈希表/字典中的键。 但是当您添加键值组合时,它必须能够生成键的哈希。 如果你的值被包裹在 Nullable 中也没关系,null.GetHashCode 是不可能的。

Dictionary 的 Indexer 属性和 Add 方法会检查 null,当找到 null 时会抛出异常。

【讨论】:

但是根据定义,哈希码并不是唯一的。 public override int GetHashCode() return 0; 是一个有效的实现,即使毫无用处。因此,如果 Dictionary 只是在内部对空值使用任意哈希码,则无关紧要。我不相信这是一个很好的解释。 然而 null 不等于 null。对 null 使用任意代码会导致一些不可预测的代码。 我不同意,我相信 null 等于 null。如果a==a必须返回true,那么null==null也必须返回true;不?此外,如果两者都为 null,则 MSDN 中提供的 example implementation 返回 true。我是不是想错了?【参考方案4】:

您通常必须回到 C++ 方法和技术,才能充分了解 .NET Framework 如何以及为何以特定方式工作。

在 C++ 中,您通常必须选择一个不会使用的键 - 字典使用该键来指向已删除和/或空条目。例如,您有一个&lt;int, int&gt; 的字典,在插入一个条目后,您将其删除。而不是立即运行垃圾清理,重组字典并导致性能不佳;字典只会用您之前选择的键替换 KEY 值,基本上意味着“当您遍历字典内存空间时,假装这个 &lt;key,value&gt; 对不存在,随意覆盖它。”

这样的键也用于以特定方式在存储桶中预分配空间的字典中-您需要一个键来“初始化”存储桶,而不是为每个条目设置一个标志来指示其内容是否为有效的。因此,您将拥有一个元组 &lt;key, value&gt; 而不是三元组 &lt;key, value, initialized&gt;,其规则是如果 key == empty_key 则它尚未初始化 - 因此您可能不会使用 empty_key 作为有效的 KEY 值。

您可以在此处的文档中的 Google 哈希表(.NET 人员的字典 :) 中看到这种行为:http://google-sparsehash.googlecode.com/svn/trunk/doc/dense_hash_map.html

查看set_deleted_keyset_empty_key 函数以了解我在说什么。

我敢打赌 .NET 使用 NULL 作为唯一的 deleted_key 或 empty_key 来执行这些提高性能的巧妙技巧。

【讨论】:

有见地。但是Dictionary&lt;int, object&gt; 呢?它不能使用null 作为键,它接受default(int) 作为键;也许它在内部使用Nullable&lt;int&gt; 作为键而不是int 嗯,int 不是可以为空的类型,所以你自然不能使用 int(null) 作为键。 ...是的;让我试着改写一下。那么用于标记已删除条目的“不会使用的密钥”是什么?除非内部键类型是框入Nullable&lt;T&gt; 的选定值类型,否则没有可用键,因为所有键都有效。没有? 非常好的问题 - 你是对的。但是将一个键留作空(并且可能另一个键被删除)是一种优化。我显然对内部 .NET 代码及其实现方式一无所知,但是 a) 不难想象这种优化仅用于引用类型,并且 b) 可能完全不同的哈希表实现用于值和指针由于它们不同的要求和性能特征而在内部使用类型?只是猜测。 查看source,当您删除条目时,它确实将密钥设置为default&lt;TKey&gt;....但我不确定它是否使用它来决定一个位置是否是免费的(我看到很多 -1 值,I think might do that)。【参考方案5】:

由于多种原因,字典不能接受空引用类型,尤其是因为它们没有 GetHashCode 方法。

可空值类型的空值旨在表示空值——语义应尽可能与引用空值同义。如果您可以使用可空的可空值而您不能使用空引用,这只是因为可空值类型的实现细节而有点奇怪。

那个或字典有:

if (key == null)

他们从来没有真正考虑过。

【讨论】:

【参考方案6】:

如果您有 Dictionary&lt;SomeType, string&gt;SomeType 作为引用类型,并且您尝试将 null 作为键传递,它会告诉您同样的事情,它不会只影响像 bool? 这样的可空类型.您可以使用任何类型作为键,无论是否可以为空。

这一切都归结为您无法真正比​​较nulls。我假设无法将 null 放在键中的逻辑,旨在与其他对象进行比较的属性是它使比较 null 引用变得不连贯。

如果您想从规范中找到原因,可以归结为“密钥不能为空引用”on MSDN。

如果您想要一个可能的解决方法的示例,您可以尝试类似于Need an IDictionary implementation that will allow a null key

【讨论】:

错了。您可以在设置为 null 的 Nullable 上调用 GetHashcode。结果为 0。 我从来没有说过你不能在Nullable&lt;bool&gt;上打电话给GetHashCode(),我说过你不能在null上打电话给GetHashCode() 尽管如此,您可以将 null 传递给 IEQualityComparer 的 GetHashCode(),因此您肯定可以得到 null 的哈希码。 @Dynami Le-Savard 但是你可以比较空值。 null == nullnull != (bool?)truenull != (bool?)false 等。就我而言,我有一个二维字典,其中一个索引器可以为空。我需要通过设计来解决这个限制,这很好。但我想了解这个限制,从这个角度来看,你的回答并没有回答 OP 的问题——它只是重新陈述了它。我们知道它不能为空,但是为什么 Null 应该被允许作为键。它的哈希码显然应该为零,您可以在 C# 中将 null 与 null 进行比较;他们是平等的。这不是带有一些奇怪的 null != null 概念的 TSQL。这个实现很糟糕。例如,我想从 item->parent 关系的平面列表构建一棵树,所以我想调用 items.GroupBy(x =&gt; x.ParentId).ToDictionary(x =&gt; x.Key, x.ToList()),但我不能,因为根节点的 ParentId 为 null,虽然它可以 GroupBy 值null 很好,它拒绝允许它作为字典中的键。荒谬的。 j/k【参考方案7】:

你不能使用空布尔值吗?因为可空类型的作用类似于引用类型。您也不能将空引用用作字典键。

您不能将空引用用作字典键的原因可能归结为 Microsoft 的设计决定。允许空键需要检查它们,这使得实现更慢和更复杂。例如,实现必须避免在空引用上使用 .Equals 或 .GetHashCode。

我同意允许空键是可取的,但现在改变行为为时已晚。如果您需要一种解决方法,您可以使用允许的空键编写自己的字典,或者您可以编写一个包装器结构,该结构隐式转换为/从 T 并使其成为您字典的键类型(即结构将包装 null 和处理比较和散列,因此字典永远不会“看到”空值)。

【讨论】:

【参考方案8】:

啊,泛型代码的问题。通过反射器考虑这个块:

private void Insert(TKey key, TValue value, bool add)

    int freeList;
    if (key == null)
    
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    

没有办法重写这段代码说“允许为空的空值,但不允许引用类型为空”。

好的,那么如何不让 TKey 成为“布尔值”呢?再说一遍,C# 语言中没有任何东西可以让你这么说。

【讨论】:

typeof(T).IsValueType 是否会为 bool 返回 true? (或任何 Nullable 类型),因此可以确定它是可空值还是引用空值。这又让它回到了实施决策【参考方案9】:

键值必须唯一,所以 null 不能是有效的键,因为 null 表示没有键。

这就是 .Net 框架不允许允许 null 值并引发异常的原因。

至于为什么 Nullable 允许,而不是在编译时捕获,我认为原因是因为 where 子句允许每个 T 除了 Nullable 一个是不可能的(至少我不知道如何实现)。

【讨论】:

但在我看来,在这种情况下,问字典是没有意义的,给我他的密钥为空的项目。 看我的例子。有什么不明白的地方?【参考方案10】:

根据 MSDN 页面,不使用 null 是合同的一部分:http://msdn.microsoft.com/en-us/library/k7z0zy8k.aspx

我猜原因是使用 null 作为有效值只会无缘无故地使代码复杂化。

【讨论】:

【参考方案11】:

.NET 中的字典键不能为空,无论键的类型如何(可为空或其他)。

来自 MSDN:只要将对象用作 Dictionary)>) 中的键,它就不能以任何影响其哈希值的方式发生变化。 Dictionary)>) 中的每个键都必须根据字典的相等比较器是唯一的。如果值类型 TValue 是引用类型,则键不能是空引用(在 Visual Basic 中为 Nothing),但值可以是空引用。 (http://msdn.microsoft.com/en-us/library/xfhwa508.aspx)

【讨论】:

是的,我们已经知道了。问题是他们为什么选择以这种方式实施。 这个问题是针对可空类型的,我强调它们的行为与引用类型相同 它们的行为不同。例如,可以获取结构体的hascode为null,但不能获取引用的null。 没有禁止使用带有愚蠢实现 GetHashCode 的类型。例如,您可以创建自己的类型,对于 GetHashCode,该类型始终返回 1,或返回随机值。您仍然可以将此类型用作 Dictionary 键。当然,你不应该这样做,但这不同于说你不能

以上是关于为啥不能使用 null 作为 Dictionary<bool?, string> 的键?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能用字符串为 Dictionary<String, Int> 下标?

为啥 Swift Dictionary 不可桥接?

Dictionary

为啥要使用using System.Collections.Generic

Dictionary使用(转)

为啥条件运算符不能正确地允许使用“null”来分配给可为空的类型? [复制]