Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.9k views
in Technique[技术] by (71.8m points)

.net - 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? It's not like it checks for null before calling Date, if the value is null it will throw a NullReference exception. I get why .HasValue can work,

but am unsure about why we need .Value on each nulllable type?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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. *

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 . a null value type; they don't exist.

In C# 2.0 nullable value types were added. 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.

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
  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.

So the question is:

Why require .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. 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. 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 x.y is a variable if x is a variable, and otherwise, a value. That is, x.y = 123 is legal if x is a variable. 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.

How does this relate to nullable value types? If we have a nullable mutable value type 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.

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 x.y would be illegal because ref demands a variable, not a property.

It gets to be a huge freakin' mess.

In C# 2.0 the design team was trying to get generics added to the language; 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! 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. 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.

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. instead of . in C# 2.0 through 5.0, and ?. in 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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...