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

时间:2017-11-21 20:44:05

标签: c# .net nullable

尝试更好地理解为什么这是一种语言功能:

我们有:

googleapis

为什么我需要使用Value来获取可空类型的值?它不像在调用Date之前检查null,如果值为null,它将抛出NullReference异常。我明白为什么.HasValue可以工作,

但我不确定为什么我们需要.Value对每个nulllable类型?

2 个答案:

答案 0 :(得分:15)

首先让我们澄清这个问题。

在C#1.0中,我们有两大类的类型:值类型,它们永远不为null,引用类型可以为空。 *

值和引用类型都支持成员访问运算符.,它选择与实例关联的值。

对于引用类型,.运算符与接收者的可为空性之间的关系是:如果接收者是空引用,则使用.运算符会产生异常。由于C#1.0值类型首先不可为空,因此无需指定.空值类型时会发生什么;他们不存在。

在C#2.0中添加了可空值类型。就其内存表示而言,可空值的类型并不神奇;它只是一个带有值实例的结构,而bool则表示它是否为null。

有一些编译器魔法(**),因为可以为空的值类型带有提升语义。通过提升,我们的意思是对可空值类型的操作具有&#34的语义;如果该值不为null,则对值进行操作并将结果转换为可空类型;否则结果为空"。 (***

也就是说,如果我们有

int? x = 2;
int? y = 3;
int? z = null;
int? r = x + y; // nullable 5
int? s = y + z; // null

在幕后,编译器正在做各种魔术以有效地实现提升算术; see my lengthy blog series on how I wrote the optimizer if this subject interests you

但是,.运算符解除。它可能是!至少有两种可能的设计是有意义的:

  1. nullable.Whatever()可以表现为可以为空的引用类型:如果接收方为null,则抛出异常,或
  2. 它可能表现得像可空算术:如果nullable为null,则忽略对Whatever()的调用,结果为Whatever()返回的任何类型的null。
  3. 所以问题是:

      

    .Value.运算符设计合理的工作并提取基础类型的成员时,为什么需要.

    好。

    注意我刚刚在那里做了什么。 有两种可能性,它们都具有完美的意义,并且与语言的既定且易于理解的方面一致,并且它们相互矛盾。语言设计师发现自己总是在中。我们现在处于这样一种情况:完全不明显的是,可引用值类型的.在引用类型上的行为是.,还是.的行为应该像+在一个可空的int。两者都是合理的。无论选择哪一个,都会有人认为这是错误的。

    语言设计团队考虑了明确的替代方案。例如,"猫王" ?.运算符,它明确地被提升为可以为空的成员访问权限。这被考虑用于C#2.0但被拒绝,然后最终添加到C#6.0。还考虑了一些其他的句法解决方案,但所有这些解决方案都因历史遗失而被拒绝。

    我们已经看到我们已经在价值类型.上获得了潜在的设计雷区,但是等等,它会变得更糟。

    现在考虑应用于值类型的.的另一个方面:如果值类型是糟糕的可变值类型,并且成员是< em> field ,如果x.y是变量,则x变量,否则为值。也就是说,如果x.y = 123是变量,则x是合法的。但是如果x不是变量,则C#编译器不允许赋值,因为将对值的副本进行赋值。

    这与可空值类型有何关系?如果我们有一个可空的可变值类型X?那么

    是什么
    x.y = 123
    

    做什么?请记住,x实际上是不可变类型Nullable<X>的一个实例,因此,如果这意味着x.Value.y = 123,那么我们正在改变返回值的副本通过Value属性,这似乎非常非常错误。

    那我们该怎么办?可以为空的值类型本身是否可变?这种突变会如何起作用?它是复制拷贝出来的语义吗?这意味着ref x.y将是非法的,因为ref需要变量,而不是属性。

    它会成为一个巨大的怪胎&#39;乱七八糟即可。

    在C#2.0中,设计团队试图将泛型添加到该语言中;如果您曾尝试将泛型添加到现有类型系统,那么您就知道它有多少工作量。如果你没有,那么,它需要做很多工作。我认为设计团队可以通过决定解决所有这些问题,并使.对可空值类型没有特殊意义。 &#34;如果你想要价值,那么你可以拨打.Value&#34;这样做的好处是不需要设计团队的特殊工作!同样地,&#34;如果使用可变的可空值类型会受到伤害,那么可能会停止这样做&#34;设计师的成本很低。

    如果我们生活在一个完美的世界中,那么我们在C#1.0中将有两种正交类型:引用类型与值类型,以及可空类型与非可空类型。我们得到的是C#1.0中的可空引用类型和非可空值类型,C#2.0中的可空值类型,以及C#8.0中有类型的非可空引用类型,十年半之后。

    在完美的世界中,我们将整理所有运算符语义,提升语义,变量语义等,一次以使它们保持一致。

    但是,嘿,我们不是生活在那个完美的世界里。我们生活在一个完美是善的敌人的世界,你必须在C#2.0到5.0中说.Value.而不是.,在C#6.0中说?.

    *我故意忽略指针类型,它们可以为空,具有值类型的一些特征和引用类型的某些特征,并且有自己的特殊运算符用于解除引用和成员访问。

    **还有一些神奇的东西:可空值类型不符合值类型约束,可空值类型框可以为空引用或盒装底层类型,以及许多其他小特殊行为。但内存布局并不神奇;它只是一个布尔旁边的值。

    ***功能程序员当然知道这可能是monad上的绑定操作。

答案 1 :(得分:4)

这是由于如何实现可空类型。

问号语法只转换为Nullable<T>,这是一个你自己可以很好地编写的结构(除了…?语法是这种类型的语言特征之外)。

Nullable<T>的.NET Core实现是开源的,its code有助于解释这一点。

Nullable<T>只有一个布尔字段和一个基础类型的值字段,只是在访问.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,因此强制转换隐藏了对属性的访问权限:

    public static explicit operator T(Nullable<T> value)
    {
        return value.Value;
    }

还有一个运算符用于反向赋值,因此DateTime? nullableNow = DateTime.Now会调用:

    public static implicit operator Nullable<T>(T value)
    {
        return new Nullable<T>(value);
    }