繁体   English   中英

C#中可空类型的价值点是什么

[英]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

在后台,编译器正在做各种魔术以有效地实现提升的算法。 如果这个主题引起您的兴趣,请参阅我冗长的博客系列,介绍如何编写优化器

但是, . 操作员解除。 它可能是! 至少有两种可行的设计是有意义的:

  1. nullable.Whatever()可以像可为空的引用类型一样执行:如果接收者为null,则抛出异常,或者
  2. 它的行为可能类似于可为空的算术:如果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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM