[英]What Is The Point of Value on Nullable Types In C#
试图更好地理解为什么这是语言功能:
我们有:
public static DateTime? Date { get; set; }
static void Main(string[] args)
{
Date = new DateTime(2017, 5, 5);
Console.WriteLine(Date.Value.Date);
Console.Read();
}
为什么需要使用Value从可为空的类型中获取值? 它不像在调用Date之前检查是否为null,如果该值为null则将引发NullReference异常。 我明白为什么.HasValue可以工作,
但是不确定为什么每个空值类型都需要.Value吗?
首先让我们澄清这个问题。
在C#1.0中,我们有两大类类型:值类型(永远不为null)和引用类型(为可空)。 *
值类型和引用类型均支持成员访问运算符 .
,它选择与实例关联的值。
对于引用类型,之间的关系.
运算符,并且接收方的可为空性是:如果接收方为空引用,则使用.
运算符产生异常。 由于C#1.0值类型首先不可为空,因此无需指定当您时会发生什么.
空值类型; 他们不存在。
在C#2.0中,添加了可为空的值类型。 就其在内存中的表示而言,可空值类型并不是什么神奇的东西。 它只是一个带有值实例的结构,并且有一个布尔值说明它是否为null。
尽管有一些可编译的魔术( **
),因为可空值类型伴随着语义的提升 。 通过提升,我们的意思是对可空值类型进行的操作的语义是“如果值不为空,则对值进行操作并将结果转换为可为空的类型;否则结果为空”。 ( ***
)
也就是说,如果我们有
int? x = 2;
int? y = 3;
int? z = null;
int? r = x + y; // nullable 5
int? s = y + z; // null
在后台,编译器正在做各种魔术以有效地实现提升的算法。 如果这个主题引起您的兴趣,请参阅我冗长的博客系列,介绍如何编写优化器 。
但是, .
操作员未解除。 它可能是! 至少有两种可行的设计是有意义的:
nullable.Whatever()
可以像可为空的引用类型一样执行:如果接收者为null,则抛出异常,或者 nullable
为null,则将忽略对Whatever()
的调用,并且结果将为返回了Whatever()
任何类型的null。 所以问题是:
为什么要求
.Value.
当有一个明智的设计.
运算符只是工作并提取基础类型的成员?
好。
注意我刚才在那做什么。 有两种可能性都非常有意义,并且与该语言的既定且易于理解的方面一致,并且彼此矛盾 。 语言设计者发现自己在这进退两难的所有时间 。 我们现在处在一种情况是否完全不清楚的情况下.
在可空值类型上的行为应类似于.
在引用类型上,或者是否.
在可为null的int上的行为应类似于+
。 两者都是合理的。 无论选择哪一个,都会有人认为这是错误的。
语言设计团队考虑了其他选择,例如使其明确。 例如,“猫王” ?.
运算符,明确是提升为可空的成员访问权限。 C#2.0曾考虑使用此方法,但此方法被拒绝,然后最终添加到C#6.0中。 还考虑了其他一些句法解决方案,但由于历史原因而被全部拒绝。
我们已经看到我们有一个潜在的设计雷区.
关于值类型,但是等等,情况变得更糟。
现在让我们考虑的另一方面.
当应用于值类型时:如果值类型是可怕的可变值类型 ,并且成员是field ,则xy
是变量(如果x
是变量),否则是值。 也就是说,如果x
是变量,则xy = 123
是合法的。 但是,如果x
不是变量,则C#编译器将不允许该赋值,因为该赋值将被复制到value的副本上 。
这与可为空的值类型有何关系? 如果我们有一个可为空的可变值,则键入X?
那是什么
x.y = 123
做? 请记住, x
确实是不可变类型Nullable<X>
的实例,因此,如果这意味着x.Value.y = 123
那么我们正在对Value
属性返回的Value
的副本进行变异 ,这似乎非常非常错误。
那么我们该怎么办? 可空值类型本身应该可变吗? 这种变异将如何起作用? 它是复制进复制出语义吗? 这将意味着ref xy
将是非法的,因为ref
需要变量而不是属性。
这将是一个巨大的怪胎 。
在C#2.0中,设计团队试图将泛型添加到该语言中。 如果您曾经尝试将泛型添加到现有的类型系统中,那么您会知道它的工作量。 如果您还没有,那么,这是很多工作。 我认为设计团队可以决定是否在所有这些问题上做出决定.
对于可空值类型没有特殊含义。 “如果您想要值,那么您就可以调用.Value
”,其好处是不需要设计团队的任何特殊工作! 同样,“如果使用可变的可为空的值类型很麻烦,那么也许停止这样做”对于设计人员来说是低成本的。
如果我们生活在一个完美的世界中,那么在C#1.0中我们将拥有两种正交类型:引用类型与值类型,以及可空类型与不可空类型。 十年半之后,我们得到的是C#1.0中的可为空的引用类型和不可为空的值类型,C#2.0中的可为空值类型以及C#8.0中的kinda-sorta非可为空的引用类型。
在完美的世界里,我们会整理出所有的操作语义,起重语义,变量语义等, 一下子就使它们保持一致。
但是,嘿,我们没有生活在这个完美的世界中。 我们生活在一个完美的世界是美好的敌人的世界,您必须说.Value.
代替.
在C#2.0到5.0,以及?.
在C#6.0中。
*
我故意忽略了指针类型,这些指针类型可以为空,并且具有值类型的某些特性和引用类型的某些特性,并且它们具有用于取消引用和成员访问的自己的特殊运算符。
**
诸如此类的东西也有魔术:可空值类型不满足值类型约束,可空值类型框为空引用或装箱的基础类型,以及许多其他小的特殊行为。 但是内存布局并不是什么神奇的事情。 这只是布尔值。
***
函数式程序员当然会知道这是对monad的绑定操作。
这是由于如何实现可空类型。
Nullable<T>
语法仅转换为Nullable<T>
,这是您可以很好地编写自己的结构(除了…?
语法是该类型的语言功能之外)。
Nullable<T>
的.NET Core实现是开源的, 其代码有助于对此进行解释。
Nullable<T>
仅具有布尔类型的字段和基础类型的value字段,并且在访问.Value
时仅引发异常:
public readonly struct Nullable<T> where T : struct
{
private readonly bool hasValue; // Do not rename (binary serialization)
internal readonly T value; // Do not rename (binary serialization)
…
public T Value
{
get
{
if (!hasValue)
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoValue);
}
return value;
}
}
…
当执行类似DateTime aDateTime = (DateTime)nullableDateTime
/赋值时,则仅调用在同一类上定义的运算符,该运算符的工作方式与在自定义类型上定义的运算符完全相同。 该运算符仅调用.Value
因此.Value
隐藏对属性的访问:
public static explicit operator T(Nullable<T> value)
{
return value.Value;
}
还有一个用于逆分配的运算符,所以DateTime? nullableNow = DateTime.Now
DateTime? nullableNow = DateTime.Now
将调用:
public static implicit operator Nullable<T>(T value)
{
return new Nullable<T>(value);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.