简体   繁体   English

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

[英]What Is The Point of Value on Nullable Types In C#

Trying to get a better understanding of why this is a language feature: 试图更好地理解为什么这是语言功能:

We have: 我们有:

public static DateTime? Date { get; set; }


static void Main(string[] args)
{
    Date = new DateTime(2017, 5, 5);

    Console.WriteLine(Date.Value.Date);
    Console.Read();
}

Why do I need to use Value to take the value from the nullable type? 为什么需要使用Value从可为空的类型中获取值? It's not like it checks for null before calling Date, if the value is null it will throw a NullReference exception. 它不像在调用Date之前检查是否为null,如果该值为null则将引发NullReference异常。 I get why .HasValue can work, 我明白为什么.HasValue可以工作,

but am unsure about why we need .Value on each nulllable type? 但是不确定为什么每个空值类型都需要.Value吗?

First let's clarify the question. 首先让我们澄清这个问题。

In C# 1.0 we had two broad categories of types: value types, which are never null, and reference types, which are nullable. 在C#1.0中,我们有两大类类型:值类型(永远不为null)和引用类型(为可空)。 *

Both value and reference types support a member access operator , . 值类型和引用类型均支持成员访问运算符 . , which selects a value associated with an instance. ,它选择与实例关联的值。

For reference types, the relationship between the . 对于引用类型,之间的关系. operator and the nullability of the receiver is: if the receiver is a null reference then use of the . 运算符,并且接收方的可为空性是:如果接收方为空引用,则使用. operator produces an exception. 运算符产生异常。 Since C# 1.0 value types are not nullable in the first place, there's no need to specify what happens when you . 由于C#1.0值类型首先不可为空,因此无需指定当您时会发生什么. a null value type; 空值类型; they don't exist. 他们不存在。

In C# 2.0 nullable value types were added. 在C#2.0中,添加了可为空的值类型。 Nullable value types are nothing magical as far as their in-memory representation goes; 就其在内存中的表示而言,可空值类型并不是什么神奇的东西。 it's just a struct with an instance of the value and a bool saying whether it is null or not. 它只是一个带有值实例的结构,并且有一个布尔值说明它是否为null。

There is some compiler magic ( ** ) though since nullable value types come along with lifting semantics . 尽管有一些可编译的魔术( ** ),因为可空值类型伴随着语义的提升 By lifting we mean that operations on nullable value types have the semantics of "if the value is not null then do the operation on the value and convert the result to a nullable type; otherwise the result is null". 通过提升,我们的意思是对可空值类型进行的操作的语义是“如果值不为空,则对值进行操作并将结果转换为可为空的类型;否则结果为空”。 ( *** ) ***

That is to say, if we have 也就是说,如果我们有

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

Behind the scenes the compiler is doing all kinds of magic to efficiently implement lifted arithmetic; 在后台,编译器正在做各种魔术以有效地实现提升的算法。 see my lengthy blog series on how I wrote the optimizer if this subject interests you . 如果这个主题引起您的兴趣,请参阅我冗长的博客系列,介绍如何编写优化器

However, the . 但是, . operator is not lifted. 操作员解除。 It could be! 它可能是! There are at least two possible designs that make sense: 至少有两种可行的设计是有意义的:

  1. nullable.Whatever() could behave as nullable reference types do: throw an exception if the receiver is null, or nullable.Whatever()可以像可为空的引用类型一样执行:如果接收者为null,则抛出异常,或者
  2. it could behave like nullable arithmetic: if nullable is null then the call to Whatever() is elided and the result is a null of whatever type Whatever() would have returned. 它的行为可能类似于可为空的算术:如果nullable为null,则将忽略对Whatever()的调用,并且结果将为返回了Whatever()任何类型的null。

So the question is: 所以问题是:

Why require .Value. 为什么要求.Value. when there is a sensible design for the . 当有一个明智的设计. operator to just work and extract a member of the underlying type? 运算符只是工作并提取基础类型的成员?

Well. 好。

Notice what I just did there. 注意我刚才在那做什么。 There are two possibilities that both make perfect sense and are consistent with an established, well-understood aspect of the language, and they contradict each other . 有两种可能性都非常有意义,并且与该语言的既定且易于理解的方面一致,并且彼此矛盾 Language designers find themselves in this cleft stick all the time . 语言设计者发现自己在这进退两难的所有时间 We are now in a situation where it is totally unobvious whether . 我们现在处在一种情况是否完全不清楚的情况下. on a nullable value type should behave like . 在可空值类型上的行为应类似于. on a reference type, or whether . 在引用类型上,或者是否. should behave like + on a nullable int. 在可为null的int上的行为应类似于+ Both are plausible. 两者都是合理的。 Whichever one is picked, someone will think it is wrong. 无论选择哪一个,都会有人认为这是错误的。

The language design team considered alternatives such as making it explicit. 语言设计团队考虑了其他选择,例如使其明确。 For example, the "Elvis" ?. 例如,“猫王” ?. operator that is explicitly a lifted-to-nullable member access. 运算符,明确是提升为可空的成员访问权限。 This was considered for C# 2.0 but rejected, and then eventually added to C# 6.0. C#2.0曾考虑使用此方法,但此方法被拒绝,然后最终添加到C#6.0中。 There were a few other syntactic solutions considered but all were rejected for reasons lost to history. 还考虑了其​​他一些句法解决方案,但由于历史原因而被全部拒绝。

Already we see that we've got a potential design minefield for . 我们已经看到我们有一个潜在的设计雷区. on value types, but wait, it gets worse. 关于值类型,但是等等,情况变得更糟。

Let's now consider another aspect of the . 现在让我们考虑的另一方面. when applied to a value type: if the value type is the awful mutable value type , and the member is a field , then xy is a variable if x is a variable, and otherwise, a value. 当应用于值类型时:如果值类型是可怕的可变值类型 ,并且成员是field ,则xy变量(如果x是变量),否则是值。 That is, xy = 123 is legal if x is a variable. 也就是说,如果x是变量,则xy = 123是合法的。 But if x is not a variable then the C# compiler disallows the assignment because the assignment would be made to a copy of the value . 但是,如果x不是变量,则C#编译器将不允许该赋值,因为该赋值将被复制到value的副本上

How does this relate to nullable value types? 这与可为空的值类型有何关系? If we have a nullable mutable value type X? 如果我们有一个可为空的可变值,则键入X? then what does 那是什么

x.y = 123

do? 做? Remember, x really is an instance of the immutable type Nullable<X> , so if this means x.Value.y = 123 then we are mutating the copy of the value returned by the Value property , which seems very, very wrong. 请记住, x确实是不可变类型Nullable<X>的实例,因此,如果这意味着x.Value.y = 123那么我们正在对Value属性返回的Value的副本进行变异 ,这似乎非常非常错误。

So what do we do? 那么我们该怎么办? Should nullable value types be mutable themselves? 可空值类型本身应该可变吗? How would that mutation work? 这种变异将如何起作用? Is it copy-in-copy-out semantics? 它是复制进复制出语义吗? That would mean that ref xy would be illegal because ref demands a variable, not a property. 这将意味着ref xy将是非法的,因为ref需要变量而不是属性。

It gets to be a huge freakin' mess . 这将是一个巨大的怪胎

In C# 2.0 the design team was trying to get generics added to the language; 在C#2.0中,设计团队试图将泛型添加到该语言中。 if you've ever tried adding generics to an existing type system, you know how much work that is. 如果您曾经尝试将泛型添加到现有的类型系统中,那么您会知道它的工作量。 If you haven't, well, it's a lot of work. 如果您还没有,那么,这是很多工作。 I think the design team can be given a pass for deciding to punt on all these issues and make . 我认为设计团队可以决定是否在所有这些问题上做出决定. have no special meaning on nullable value types. 对于可空值类型没有特殊含义。 "If you want the value then you call .Value " has the benefit of requiring no particular work on the part of the design team! “如果您想要值,那么您就可以调用.Value ”,其好处是不需要设计团队的任何特殊工作! And similarly, "if it hurts to use mutable nullable value types, then maybe stop doing that" is low cost for the designers. 同样,“如果使用可变的可为空的值类型很麻烦,那么也许停止这样做”对于设计人员来说是低成本的。


If we lived in a perfect world then we would have had two orthogonal kinds of types in C# 1.0: reference types vs value types, and nullable types vs non-nullable types. 如果我们生活在一个完美的世界中,那么在C#1.0中我们将拥有两种正交类型:引用类型与值类型,以及可空类型与不可空类型。 What we got was nullable reference types and non-nullable value types in C# 1.0, nullable value types in C# 2.0, and kinda-sorta non-nullable reference types in C# 8.0, a decade and a half later. 十年半之后,我们得到的是C#1.0中的可为空的引用类型和不可为空的值类型,C#2.0中的可为空值类型以及C#8.0中的kinda-sorta非可为空的引用类型。

In that perfect world we would have sorted out all the operator semantics, lifting semantics, variable semantics, and so on, all at once to make them consistent. 在完美的世界里,我们会整理出所有的操作语义,起重语义,变量语义等, 一下子就使它们保持一致。

But, hey, we don't live in that perfect world. 但是,嘿,我们没有生活在这个完美的世界中。 We live in a world where the perfect is the enemy of the good, and you have to say .Value. 我们生活在一个完美的世界是美好的敌人的世界,您必须说.Value. instead of . 代替. in C# 2.0 through 5.0, and ?. 在C#2.0到5.0,以及?. in C# 6.0. 在C#6.0中。


* I'm deliberately ignoring pointer types, which are nullable and have some characteristics of value types and some characteristics of reference types, and which have their own special operators for dereferencing and member access. *我故意忽略了指针类型,这些指针类型可以为空,并且具有值类型的某些特性和引用类型的某些特性,并且它们具有用于取消引用和成员访问的自己的特殊运算符。

** There is also magic in stuff like: nullable value types do not fulfill the value type constraint, nullable value types box to null references or boxed underlying types, and many other small special behaviours. **诸如此类的东西也有魔术:可空值类型不满足值类型约束,可空值类型框为空引用或装箱的基础类型,以及许多其他小的特殊行为。 But the memory layout is nothing magical; 但是内存布局并不是什么神奇的事情。 it's just a value beside a bool. 这只是布尔值。

*** Functional programmers will of course know that this is the bind operation on the maybe monad. ***函数式程序员当然会知道这是对monad的绑定操作。

This is due to how nullable types are implemented. 这是由于如何实现可空类型。

The questionmark syntax only translates into Nullable<T> , which is a struct you could very well write yourself (except for the …? syntax being a language feature for this type). Nullable<T>语法仅转换为Nullable<T> ,这是您可以很好地编写自己的结构(除了…?语法是该类型的语言功能之外)。

The .NET Core implementation of Nullable<T> is open source and its code helps explaining this. Nullable<T>的.NET Core实现是开源的, 其代码有助于对此进行解释。

Nullable<T> only has a boolean field and a value field of the underlying type and just throws an exception when accessing .Value : 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;
        }
    }
…

When you do a cast / assignment like DateTime aDateTime = (DateTime)nullableDateTime , then you only call an operator that is defined on the same class which works exactly like an operator defined on a custom type. 当执行类似DateTime aDateTime = (DateTime)nullableDateTime /赋值时,则仅调用在同一类上定义的运算符,该运算符的工作方式与在自定义类型上定义的运算符完全相同。 This operator only calls .Value so the cast hides the access to the property: 该运算符仅调用.Value因此.Value隐藏对属性的访问:

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

There also is an operator for the inverse assignment, so DateTime? nullableNow = DateTime.Now 还有一个用于逆分配的运算符,所以DateTime? nullableNow = DateTime.Now DateTime? nullableNow = DateTime.Now would call: 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