Nullable<T> 概念混淆

10

为什么以下操作被禁止?

Nullable<Nullable<int>>

然而

struct MyNullable <T>
{


}

MyNullable<Nullable<int>> 

不是


一个 Nullable<Nullable<int>> 的意思是什么? - configurator
2
说实话,在这里,“意义”有点无关紧要。你不应该读出任何事物的名字,因为编译器并不关心。但是这里的不同之处在于,CLR将System.Nullable<T>作为一种特殊类型来处理,并对它进行了某些操作的不同处理。 - Curt Hagenlocher
5个回答

19
这是因为结构约束实际上意味着'非空',因为Nullable虽然是一个结构体,但可为空(可以接受null值),因此 Nullable<int>不是外部Nullable的有效类型参数。 这在约束文档中明确说明。
其中T:struct 类型参数必须是值类型。除了Nullable之外的任何值类型都可以指定。 有关详细信息,请参阅使用可空类型(C#编程指南)。
如果您想要理解原因,您需要实际语言设计者的评论,但我找不到。然而,我认为:
1. 实现当前形式的Nullable所需的编译器和平台更改相当广泛(并且是2.0版本发布的相对最后一分钟的添加)。 2. 它们具有几个潜在的混淆边缘情况。
允许int??的等效项只会使其更加混乱,因为该语言没有区分Nullable<Nullable<null>>和Nullable<null>的方法,也没有明显的解决方案。
Nullable<Nullable<int>> x = null;
Nullable<int> y = null;
Console.WriteLine(x == null); // true
Console.WriteLine(y == null); // true
Console.WriteLine(x == y); // false or a compile time error!

使该返回值为true将非常复杂,并且会在涉及Nullable类型的许多操作中产生显著的开销。

CLR中的某些类型是“特殊的”,例如字符串和基元,编译器和运行时都了解彼此使用的实现方式。 Nullable也以这种方式很特殊。由于它已经在其他领域被特殊处理,所以特殊处理where T:struct方面并不是一件大事。其好处在于处理泛型类中的结构,因为除了Nullable之外,它们都不能与null进行比较。这意味着jit可以安全地认为t == null始终为false。

当语言被设计为允许两个非常不同的概念相互作用时,您往往会遇到奇怪,令人困惑或非常危险的边缘情况。例如,考虑Nullable和等式运算符。

int? x = null;
int? y = null;
Console.WriteLine(x == y); // true
Console.WriteLine(x >= y); // false!     

通过在使用结构泛型约束时防止Nullables,可以避免许多令人讨厌(且不清晰)的边缘情况。至于规范的确切部分要求这一点见第25.7节(强调是我的):
引用块: 值类型约束指定用于类型参数的类型参数必须是值类型(§25.7.1)。任何非可空结构类型、枚举类型或具有值类型约束的类型参数都满足此约束。具有值类型约束的类型参数不得同时具有构造函数约束。System.Nullable类型为T指定了非可空值类型约束。因此,禁止递归构造类型T??和Nullable>的形式。

出于好奇,将Nullable<T>作为引用类型并使其与装箱的T同义是否会有任何问题?除了以下事实:(1)IsValueType对可空类型报告true,(2)非null的装箱值类型需要堆对象来保存数据,而可空值类型在类型本身内使用额外的存储空间;(3)不允许在类约束泛型中使用可空类型,可空类型和装箱类型之间有什么语义差异? - supercat
@supercat,请记住CLR和c#可以理解Nullable<T>并且可以在null和default(Nullable<T>)之间进行转换。如果它是引用类型,您将会遇到一些真正的问题。此外,如果接受它作为引用类型,使用装箱形式可能对许多情况都是可接受的。如果某人拥有具有值复制语义的类型,则可能也希望具有内联分配等其他行为,并且他们可能不希望在可空上下文中使用时将其变成引用类型。 - ShuggyCoUk
如果可空值类型是可变的,那么语义将与引用类型非常不同。除了内存使用之外,我无法想到任何不可变值类型和不可变引用类型之间的语义差异;由于可空类型本来就不是很节省空间,我不确定内存是否会成为一个大问题。系统喜欢在足够多的其他上下文中将值类型转换为引用类型(装箱),这实际上确实会创建问题,但我不确定装箱可空类型是否存在问题。 - supercat
如果将一个非空变量独立地复制到几个不同的可空变量中,把它们存储为值类型可以避免独立装箱。另一方面,如果将一个值复制到一个引用类型的可空double中,然后再复制到其他可空double中,则每个引用将占用四个字节,而值类型double将占用八个字节。我认为实现可空类型作为值类型最强有力的论据是在调用站点对值类型隐式转换为可空类型。 - supercat
@ShuggyCoUk:我个人认为将可变值类型隐式转换为引用类型比将不可变值类型转换为引用类型更加邪恶。与Eric Lippert所说的相反,可变值类型并不邪恶,并且具有有用的语义,其他方式无法以同样高效的方式实现。我有时希望有一个Boxed<T>,它可以作为T通常使用,但在转换为接口时不需要额外的装箱;这样的东西可以实现与Nullable<T>非常相似的语义。 - supercat
显示剩余14条评论

14

1
Nullable仍然是一个值类型,只不过它是可空的。 - ShuggyCoUk
1
你说得对。我没有意识到这一点。我以为它可以与null进行比较,所以它必须是一个引用类型,但实际上它是一个结构体。我已经更正了我的答案。 - recursive
常见误解,根据其他答案判断 :) - ShuggyCoUk

3

Nullable是特殊的,因为CLR内置了对Nullable类型的装箱和拆箱的显式支持:

如果您针对Nullable<T>使用MSIL box指令,则实际上会得到一个null作为结果。没有其他值类型在装箱时会产生null。

对于拆箱也有类似而对称的支持。


很酷,它很特别,但它并没有回答为什么它是被允许的这个问题。 - Sasha
1
为什么允许使用 which? Nullable<Nullable<int>> 明显是 允许的,这是因为 Nullable 的“特殊性”与此不兼容。 MyNullable<Nullable<int>> 是允许的,因为它与CLR的特殊处理没有冲突。 - Curt Hagenlocher

2

Nullable的通用类型参数本身必须是非空类型(即值类型)。这是我得到的C#编译器警告,似乎很有道理。告诉我,为什么你要这样做呢?我个人看不出有什么用处,甚至意义也不大。


我经常在可能存在真/假/未知状态时使用bool?。当来自SQL世界时非常有效。 - andleer
我并不打算使用它,只是觉得它违反了不同值类型泛型的类似限制。这里Nullable特殊吗? - Sasha
嘿,我觉得如果你曾经有过实现这样的想法的愿望,那么你可能需要重新学习编程。 - Noldorin
@Robinson,但是一个可空的可空布尔值?这就是问题所在。我看到了三个布尔值(真,假,文件未找到)的好处。 - strager
@Sasha:我不会说Nullable在这里有多特别(虽然它在C#中有一个内置的语言特性)。泛型类型约束并不是非常常见,但也不是极为罕见的(你可以使用类型声明上的“where”子句自己创建它们)。 - Noldorin
显示剩余2条评论

0

Nullable 允许您将值类型转换为引用类型,就像引用类型一样,该值要么存在,要么不存在(为空)。由于引用类型已经是可空的,因此不允许使用。

引用自 MSDN

Nullable(T) 结构支持仅使用值类型作为可空类型,因为引用类型在设计上就是可空的。


它并不像引用类型一样,只是为其赋予了额外的特殊值和编译器支持。复制语义保持不变(这是主要区别)。 - ShuggyCoUk
@Shuggy -- 再次引用同一参考文献:表示其基础类型为值类型的对象,该对象还可以像引用类型一样分配空引用(在 Visual Basic 中为 Nothing)。 - tvanfosson
只有这种方式才能实现,需要相当多的编译器魔法。你的回答有误导性,因为它暗示它使其成为引用类型,实际上是结构体约束被特殊处理了。 - ShuggyCoUk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接