为什么不能将null用作Dictionary <bool ?, string>的键?


69

显然,null即使密钥是可为空的类型,也不能将a用作密钥。

这段代码:

var nullableBoolLabels = new System.Collections.Generic.Dictionary<bool?, string>
{
    { true, "Yes" },
    { false, "No" },
    { null, "(n/a)" }
};

...导致此异常:

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

说明:执行当前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) +40
System.Collections.Generic.Dictionary'2.Add(TKey key, TValue value) +13

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


您为什么为此需要一本字典?切换语句可以完成相同的工作:string getLabel(bool? value) { if (value == null) { ... } else if { ... } else { ... }; }
朱丽叶

3
@Juliet,长话短说,但我会尝试:我将在三个不同的地方使用三个标签(是,否,不适用):( 1)bool? -> label转换器,(2)label -> bool?转换器和(3 )SelectList表示一个DropDownMenu(我本应该将字典值传递给它)。按照DRY,我希望将标签放在一个地方,以防万一我后来改变主意,例如说Y,N,NA。由于不允许这样做,所以我最终使用了三个const(每个标签一个)和一个字符串数组SelectList。没有那么方便,但是足够好。
devuxer'2

有时,您必须返回C ++才能查看.NET中实现的逻辑。.请看下面的答案。
Mahmoud Al-Qudsi'2

2
@Juliet,就我而言,我需要一个Guid?作为密钥。您可以将bool?视为最简单的示例。
ANeves认为SE是邪恶的,2011年

Answers:


37

如果您有一个Dictionary<SomeType, string>SomeType它是一个引用类型,并且您试图通过null作为键,它将告诉您相同的事情,它不会影响仅可空类型bool?。您可以使用任何类型作为键,是否可以为null。

一切都归结为你不能真正比较的事实nulls。我假设无法放入null密钥的逻辑背后是设计成可以与其他对象进行比较的属性,这使得比较null引用变得不连贯。

如果您希望从规格中获得理由,可以归结为MSDN上的“密钥不能为空引用” 。

如果您想要一个可能的解决方法的示例,则可以尝试类似于“需要IDictionary实现”的操作,该实现将允许使用空键


5
错误。您可以在设置为null的Nullable <bool>上调用GetHashcode。其结果是0
乔纳森·艾伦

1
我从来没有说过你不能叫GetHashCode()Nullable<bool>,我说你不能叫GetHashCode()null
Dynami Le Sa​​vard

1
不过,您可以将null传递给IEQualityComparer的GetHashCode(),因此可以确定获取null的哈希码。
乔恩·汉纳

1
@Dynami Le-Savard但是您可以比较null。null == nullnull != (bool?)truenull != (bool?)false,等。在我来说,我有一个二维字典,和索引的一个东西,可以为空。我将需要通过设计解决此限制,这很好。但我想了解局限性,因此,您的回答无法回答OP的问题-只是重申一下。我们知道它不能为null,但是为什么呢?
ANeves认为SE是邪恶的,2011年

3
空值应被允许作为密钥。它的哈希码显然应该为零,并且您可以在C#中将null与null进行比较。他们是平等的。这不是带有一些奇怪的null!= null概念的TSQL。这种实现是可怕的。例如,我想从item-> parent关系的平面列表中构建一棵树,所以我想调用items.GroupBy(x => x.ParentId).ToDictionary(x => x.Key, x.ToList()),但是我不能,因为根节点的ParentId为null,尽管它可以GroupBy值null ,它拒绝允许它作为字典中的键。荒谬。j / k
Triynko '17

14

通常,您必须返回到C ++方法论和技术,以完全了解.NET Framework如何以及为什么以特定方式工作。

在C ++中,您通常不得不选择一个不会使用的键-词典使用此键指向已删除和/或为空的条目。例如,您有一个的字典<int, int>,并在插入条目后将其删除。与其在那儿不时地运行垃圾清理,而是重组字典并导致性能下降;字典将只用您先前选择的键替换KEY值,基本上意味着“当您遍历字典的存储空间时,假装这<key,value>对不存在,可以随时覆盖它”。

这样的密钥还用在字典中,该字典以特定方式预先在存储桶中分配了空间-您需要一个密钥来“初始化”存储桶,而不是为每个条目都具有一个指示其内容是否有效的标志。因此,<key, value, initialized>您将拥有一个元组而不是三元组<key, value>,,其规则是如果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作为唯一的delete_key或empty_key来执行这些改进性能的漂亮技巧。


有见地。但是呢Dictionary<int, object>?它不能null用作密钥,并且可以接受default(int)作为密钥;也许它在Nullable<int>内部使用而不是int
ANeves认为SE是邪恶的2011年

好吧,int不是可为null的类型,因此您自然无法使用int(null)作为键。
Mahmoud Al-Qudsi 2011年

2
...是的 让我尝试改写。那么,将用于标记已删除条目的“将不使用的键”是什么?除非内部键类型是装在内的选定值类型Nullable<T>,否则没有可用的键,因为所有键均有效。没有?
ANeves认为SE是邪恶的

很好的问题-你是对的。但是将一个键留为空(可能还有另一个键被删除)是一种优化。我显然对内部.NET代码及其实现方式一无所知,但是a)不难想象这样的优化仅用于引用类型,并且b)可能完全不同的hashtable实现用于值和指针内部类型是由于它们的不同要求和性能特征?只是一个猜测。
Mahmoud Al-Qudsi'2

源头上看,它确实将密钥设置为default<TKey>删除条目时的...。但是我不确定它是用来确定现货是否免费的(我认为很多-1都可以做到这一点)。)。
杰夫·B

4

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

不能将空引用用作字典键的原因可能归结于Microsoft的设计决策。允许空键需要对其进行检查,这使实现速度更慢,更复杂。例如,实现必须避免在空引用上使用.Equals或.GetHashCode。

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


3

没有根本原因。HashSet允许为null,而HashSet只是一个Dictionary,其中键与值的类型相同。因此,实际上,应该允许使用空键,但现在会中断以更改它,因此我们一直坚持使用它。


2

词典(基本描述)
字典是.NET Framework 2.0中引入的Hashtable类的通用(类型化)实现。

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

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

放在一起,您会得到什么
。NET并不真正在乎您用作哈希表/字典中的键的内容。
但是,当您添加键值组合时,它必须能够生成键的哈希值。
您的值是否包含在Nullablenull内都没有关系。GetHashCode是不可能的。

字典的Indexer属性和Add方法将检查是否为空,并在找到空值时引发异常。


但是,根据定义,哈希码不一定是唯一的。public override int GetHashCode() { return 0; }是一个有效的实现,即使很没用。因此,如果Dictionary仅在内部将任意哈希码用于null值,则没有关系。我不认为这是一个很好的解释。
ANeves认为SE是邪恶的,2011年

但是null不等于null。对null使用任意代码将导致某些不可预测的代码。
Zyphrax

2
我不同意,我相信null等于null。如果a==a 必须返回true,则null==null还必须返回true;否则必须返回true。没有?同样,如果两者均为null,则MSDN中提供的示例实现将返回true。我在想这个吗?
ANeves认为SE是邪恶的2011年


0

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

这就是为什么Net框架不会允许值,并抛出一个异常。

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


1
但是在我看来,在这种情况下问字典是没有意义的,请给我他的键为空的项目。
Fitzchak Yitzchaki'2

2
看我的例子。这没有什么意义呢?
devuxer'2

0

嗯,通用代码的问题。通过Reflector考虑以下块:

private void Insert(TKey key, TValue value, bool add)
{
    int freeList;
    if (key == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

没有办法重写此代码,说“允许使用可为null的null,但不允许引用类型为null”。

好的,那么怎么不允许TKey成为“笨蛋”呢?再说一次,C#语言中没有什么可以让您这么说的。


typeof(T)。IsValueType将为布尔值返回true吗?(或任何Nullable类型),因此可以确定它是否是可为空的值与引用为null的值。这使它成为实施决策
jeffora 2010年

0

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

可空值类型的空值表示空值-语义应尽可能与引用空值同义。如果您可以使用可以为null的可为空的值,而不能仅仅由于实现可为空的值类型的实现细节而不能使用null引用,那将有些奇怪。

或词典具有:

if (key == null)

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


-1

在.NET中,字典键不能为null,无论键的类型(可为null还是以其他方式)。

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


1
是的,我们已经知道了。问题是为什么他们选择以这种方式实施。
乔纳森·艾伦

这个问题是特定于可空类型的,我在强调它们的行为与引用类型相同
jeffora 2010年

他们的行为不同。例如,您可以获得结构为null而不是引用为null的hascode。
乔纳森·艾伦

1
没有禁止使用带有GetHashCode的愚蠢实现的类型。例如,您可以创建自己的类型,对于GetHashCode,该类型始终返回1或返回随机值。您仍然可以将此类型用作字典键。当然,您不应该这样做,但这与说不可以不同
Ryan Lundy'2

-1

我一直在阅读有关内容。正如埃里克(Eric)回答的那样,我现在认为这是不正确的,并非所有字符串都将自动被Interned,因此我需要覆盖相等操作。


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

我处于一个简单的字符串形式的简单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查找它。


2
不,这是错误的。如果字符串是计算结果,则不对它们进行实习。您的问题是byte []不会覆盖GetHashCode和Equals,因此您可以进行引用比较,而字符串可以提供值比较。
erikkallen 2010年

当然,这就是您所期望的!仅仅因为两个对象具有相同的值并不能使它们成为同一对象。在c#中,字符串是不可变的,因此所有操作实际上都为您提供了一个新字符串
JonnyRaa 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.