【问题标题】:What Is The Point of Value on Nullable Types In C#C# 中可空类型的价值点是什么
【发布时间】:2017-11-21 20:44:05
【问题描述】:

试图更好地理解为什么这是一个语言特性:

我们有:

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?

【问题讨论】:

  • 呃,不然你怎么读值?
  • 它的类型为Nullable<DataType>。这是编译器对你隐藏的。
  • @AdamWeitzman 你可以。这就是您获得ValueHasValue 以及Nullable<T> 的所有其他成员的方式。
  • 因为DateTime? 转换为Nullable<DateTime>,它不再是DateTime 原始类型了。

标签: c# .net nullable


【解决方案1】:

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

在 C# 1.0 中,我们有两大类类型:值类型(永远不会为空)和引用类型(可以为空)。 *

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

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

在 C# 2.0 中添加了可空值类型。就它们在内存中的表示而言,可空值类型并没有什么神奇之处。它只是一个带有值实例的结构和一个表示它是否为空的布尔值。

有一些编译器魔法 (**),因为可空值类型伴随着提升语义提升我们的意思是对可空值类型的操作具有“如果值不为空,则对该值执行操作并将结果转换为可空类型;否则结果为空”的语义. (***)

也就是说,如果我们有

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() 可以像可空引用类型那样表现:如果接收者为空,则抛出异常,或者
  2. 它的行为可能类似于可空算术:如果 nullable 为空,则省略对 Whatever() 的调用,结果是 Whatever() 将返回的任何类型的空值。

所以问题是:

. 操作符设计合理时,为什么需要.Value. 才能正常工作并提取基础类型的成员?

嗯。

注意我刚刚在那里做了什么。 有两种可能性都非常合理,并且与该语言的既定、易于理解的方面一致,但它们相互矛盾。语言设计者发现自己总是陷入这种裂痕。我们现在处于完全不明显的情况下,可空值类型上的 . 是否应该像引用类型上的 . 一样,或者 . 是否应该像可空 int 上的 + 一样表现。两者都是合理的。随便选哪一个,都会有人认为是错的。

语言设计团队考虑了替代方案,例如使其明确。例如,“Elvis”?. 运算符明确是提升为可空成员访问。这被考虑用于 C# 2.0 但被拒绝,然后最终添加到 C# 6.0。考虑了一些其他语法解决方案,但都因历史原因被拒绝。

我们已经看到. 在值类型上存在潜在的设计雷区,但等等,情况会变得更糟。

现在让我们考虑. 应用于值类型时的另一个方面:如果值类型是糟糕的可变值类型,并且成员是字段,那么如果x 是一个变量,那么x.y 就是一个变量,否则就是一个值。也就是说,如果x 是变量,则x.y = 123 是合法的。但是,如果x 不是变量,则 C# 编译器不允许赋值,因为赋值将针对值的副本

这与可空值类型有何关系?如果我们有一个可以为空的可变值类型X?,那么会做什么

x.y = 123

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

那么我们该怎么办?可空值类型本身是否应该是可变的?这种突变将如何发挥作用?是copy-in-copy-out语义吗?这意味着ref x.y 是非法的,因为ref 需要一个变量,而不是一个属性。

这会变成一个巨大的混乱

在 C# 2.0 中,设计团队试图将 泛型 添加到语言中;如果您曾经尝试将泛型添加到现有类型系统中,您就会知道这需要做多少工作。如果你还没有,那么工作量很大。我认为设计团队可以通过决定解决所有这些问题并使. 对可空值类型没有特殊意义。 “如果你想要这个价值,那么你打电话给.Value”的好处是不需要设计团队做任何特别的工作!同样,“如果使用可变的可为空值类型会造成伤害,那么也许停止这样做”对于设计师来说成本很低。


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

在那个完美的世界里,我们会整理出所有的运算符语义、提升语义、变量语义等等,一次性使它们保持一致。

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


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

** 还有一些神奇的东西,比如:可空值类型不满足值类型约束,可空值类型框到空引用或装箱的基础类型,以及许多其他小的特殊行为。但是内存布局并没有什么神奇之处。它只是一个布尔值。

*** 函数式程序员当然知道这是对 Maybe monad 的绑定操作。

【讨论】:

  • 哦,天哪,这就像你的博客,但在 SO。谢谢!
  • 我的意思是,我不知道 C#9 但对于 8 .Value 仍然实现为返回只读原始副本的 getter,这仍然很糟糕,因为我们仍然不获得可变的可空值类型(即使像SpinLock? haveFun 这样的疯狂东西仍然编译得很好)。他们所要做的就是放下一对readonlys 和那个吸气剂;那么,如果您可以将Value 设置为某个值而HasValue 保持为假,那真的是一个问题吗?
【解决方案2】:

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

问号语法只翻译成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);
    }

【讨论】:

    猜你喜欢
    • 2012-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-10
    • 1970-01-01
    • 2010-10-24
    相关资源
    最近更新 更多