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 可以工作了,
但我不确定为什么我们需要在每个可为 null 的类型上使用 .Value?
这是由于可空类型的实现方式所致。
问号语法只翻译成 Nullable<T>
,这是一个你可以很好地自己编写的结构(除了 …?
语法是这种类型的语言特性)。
Nullable<T>
的 .NET Core 实现是开源的,its code 有助于解释这一点。
Nullable<T>
只有一个 boolean 字段和一个底层类型的值字段,只是在访问 .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
这样的强制转换/赋值时,您只会调用在同一 class 上定义的运算符,其工作方式与在自定义类型上定义的运算符完全相同。此运算符仅调用 .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);
}
首先让我们澄清一下问题。
在 C# 1.0 中,我们有两大类类型:值类型(从不为空)和引用类型(可为空)。 *
值和引用类型都支持成员访问运算符、.
,它选择与实例关联的值。
对于引用类型,.
运算符与接收者的可空性之间的关系是:如果接收者是空引用,则使用 .
运算符会产生异常。由于 C# 1.0 值类型首先是不可空的,因此无需指定 .
空值类型时会发生什么;它们不存在。
在 C# 2.0 中添加了可空值类型。就内存中的表示而言,可空值类型并不神奇;它只是一个带有值实例的结构和一个表示它是否为空的布尔值。
编译器有一些魔力 (**
),因为可为 null 的值类型伴随 提升语义 。通过 提升 我们的意思是对可空值类型的操作具有 "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" 的语义。 (***
)
也就是说,如果我们有
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.
但是,.
运算符未 解除。它可能是!至少有两种可能的设计是有意义的:
nullable.Whatever()
可以像可空引用类型那样工作:如果接收者为空则抛出异常,或者
- 它的行为类似于可空算法:如果
nullable
为空,则省略对 Whatever()
的调用,结果是 Whatever()
将返回的任何类型的空值。
所以问题是:
Why require .Value.
when there is a sensible design for the .
operator to just work and extract a member of the underlying type?
嗯。
注意我刚刚在那里做了什么。 有两种可能性既完全合理又与语言的既定的、易于理解的方面一致,但它们相互矛盾。语言设计者发现自己一直 处于这种状态。我们现在处于这样一种情况,即 .
在可空值类型上的行为是否应该像在引用类型上的 .
一样,或者 .
是否应该像 +
一样在可为空的 int 上。两者都是合理的。选哪个都会有人觉得不对
语言设计团队考虑了替代方案,例如使其明确化。例如,"Elvis" ?.
运算符明确是一个提升为可空的成员访问。这曾被考虑用于 C# 2.0 但被拒绝,然后最终添加到 C# 6.0。还考虑了其他一些句法解决方案,但由于已被历史遗忘的原因全部被拒绝。
我们已经看到 .
在值类型上存在潜在的设计雷区,但等等,情况会变得更糟。
现在让我们考虑 .
应用于值类型时的另一个方面:如果值类型是可怕的 可变值类型 ,并且成员是字段,如果x
是一个变量,那么x.y
是一个变量,否则就是一个值。也就是说,如果 x
是一个变量,那么 x.y = 123
是合法的。但是,如果 x
不是变量,则 C# 编译器不允许赋值,因为 赋值将针对值 .
的副本进行
这与可空值类型有何关系?如果我们有一个可为 null 的可变值类型 X?
那么
是什么
x.y = 123
做吗?请记住,x
确实是 不可变 类型 Nullable<X>
的一个实例,所以如果这意味着 x.Value.y = 123
那么 我们正在改变Value
属性 返回值的副本,这似乎非常非常错误。
那我们怎么办?可空值类型本身应该是可变的吗?该突变将如何起作用?它是复制输入复制输出语义吗?这意味着 ref x.y
是非法的,因为 ref
需要一个变量,而不是 属性.
它会变得一团糟。
在 C# 2.0 中,设计团队试图将 泛型 添加到语言中;如果您曾尝试将泛型添加到现有的类型系统,您就会知道这需要多少工作。如果你还没有,那么,这是很多工作。我认为设计团队可以通过决定在所有这些问题上下注并使 .
对可空值类型没有特殊含义。 "If you want the value then you call .Value
" 的好处是不需要设计团队的特别工作!同样,"if it hurts to use mutable nullable value types, then maybe stop doing that" 对设计者来说成本很低。
如果我们生活在一个完美的世界中,那么在 C# 1.0 中会有两种正交 类型:引用类型与值类型,以及可空类型与不可空类型.我们得到的是 C# 1.0 中的可为 null 的引用类型和不可为 null 的值类型,C# 2.0 中的可为 null 的值类型,以及 15 年后的 C# 8.0 中的有点不可为 null 的引用类型。
在那个完美的世界里,我们会把所有的运算符语义、提升语义、变量语义等等都整理出来,一下子让它们保持一致。
但是,嘿,我们并不生活在那个完美的世界中。我们生活在一个完美是善的敌人的世界,在 C# 2.0 到 5.0 中你必须说 .Value.
而不是 .
,在 C# 6.0 中说 ?.
。
*
我故意忽略指针类型,它们可以为 null,具有值类型的一些特征和引用类型的一些特征,并且它们有自己的特殊运算符用于取消引用和成员访问。
**
诸如此类的东西也很神奇:可为空的值类型不满足值类型约束、可为空的值类型装箱到空引用或装箱的基础类型,以及许多其他小的特殊行为。但是内存布局并不神奇。它只是布尔值旁边的一个值。
***
函数式程序员当然知道这是maybe monad上的bind操作
试图更好地理解为什么这是一个语言特性:
我们有:
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 可以工作了,
但我不确定为什么我们需要在每个可为 null 的类型上使用 .Value?
这是由于可空类型的实现方式所致。
问号语法只翻译成 Nullable<T>
,这是一个你可以很好地自己编写的结构(除了 …?
语法是这种类型的语言特性)。
Nullable<T>
的 .NET Core 实现是开源的,its code 有助于解释这一点。
Nullable<T>
只有一个 boolean 字段和一个底层类型的值字段,只是在访问 .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
这样的强制转换/赋值时,您只会调用在同一 class 上定义的运算符,其工作方式与在自定义类型上定义的运算符完全相同。此运算符仅调用 .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);
}
首先让我们澄清一下问题。
在 C# 1.0 中,我们有两大类类型:值类型(从不为空)和引用类型(可为空)。 *
值和引用类型都支持成员访问运算符、.
,它选择与实例关联的值。
对于引用类型,.
运算符与接收者的可空性之间的关系是:如果接收者是空引用,则使用 .
运算符会产生异常。由于 C# 1.0 值类型首先是不可空的,因此无需指定 .
空值类型时会发生什么;它们不存在。
在 C# 2.0 中添加了可空值类型。就内存中的表示而言,可空值类型并不神奇;它只是一个带有值实例的结构和一个表示它是否为空的布尔值。
编译器有一些魔力 (**
),因为可为 null 的值类型伴随 提升语义 。通过 提升 我们的意思是对可空值类型的操作具有 "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" 的语义。 (***
)
也就是说,如果我们有
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.
但是,.
运算符未 解除。它可能是!至少有两种可能的设计是有意义的:
nullable.Whatever()
可以像可空引用类型那样工作:如果接收者为空则抛出异常,或者- 它的行为类似于可空算法:如果
nullable
为空,则省略对Whatever()
的调用,结果是Whatever()
将返回的任何类型的空值。
所以问题是:
Why require
.Value.
when there is a sensible design for the.
operator to just work and extract a member of the underlying type?
嗯。
注意我刚刚在那里做了什么。 有两种可能性既完全合理又与语言的既定的、易于理解的方面一致,但它们相互矛盾。语言设计者发现自己一直 处于这种状态。我们现在处于这样一种情况,即 .
在可空值类型上的行为是否应该像在引用类型上的 .
一样,或者 .
是否应该像 +
一样在可为空的 int 上。两者都是合理的。选哪个都会有人觉得不对
语言设计团队考虑了替代方案,例如使其明确化。例如,"Elvis" ?.
运算符明确是一个提升为可空的成员访问。这曾被考虑用于 C# 2.0 但被拒绝,然后最终添加到 C# 6.0。还考虑了其他一些句法解决方案,但由于已被历史遗忘的原因全部被拒绝。
我们已经看到 .
在值类型上存在潜在的设计雷区,但等等,情况会变得更糟。
现在让我们考虑 .
应用于值类型时的另一个方面:如果值类型是可怕的 可变值类型 ,并且成员是字段,如果x
是一个变量,那么x.y
是一个变量,否则就是一个值。也就是说,如果 x
是一个变量,那么 x.y = 123
是合法的。但是,如果 x
不是变量,则 C# 编译器不允许赋值,因为 赋值将针对值 .
这与可空值类型有何关系?如果我们有一个可为 null 的可变值类型 X?
那么
x.y = 123
做吗?请记住,x
确实是 不可变 类型 Nullable<X>
的一个实例,所以如果这意味着 x.Value.y = 123
那么 我们正在改变Value
属性 返回值的副本,这似乎非常非常错误。
那我们怎么办?可空值类型本身应该是可变的吗?该突变将如何起作用?它是复制输入复制输出语义吗?这意味着 ref x.y
是非法的,因为 ref
需要一个变量,而不是 属性.
它会变得一团糟。
在 C# 2.0 中,设计团队试图将 泛型 添加到语言中;如果您曾尝试将泛型添加到现有的类型系统,您就会知道这需要多少工作。如果你还没有,那么,这是很多工作。我认为设计团队可以通过决定在所有这些问题上下注并使 .
对可空值类型没有特殊含义。 "If you want the value then you call .Value
" 的好处是不需要设计团队的特别工作!同样,"if it hurts to use mutable nullable value types, then maybe stop doing that" 对设计者来说成本很低。
如果我们生活在一个完美的世界中,那么在 C# 1.0 中会有两种正交 类型:引用类型与值类型,以及可空类型与不可空类型.我们得到的是 C# 1.0 中的可为 null 的引用类型和不可为 null 的值类型,C# 2.0 中的可为 null 的值类型,以及 15 年后的 C# 8.0 中的有点不可为 null 的引用类型。
在那个完美的世界里,我们会把所有的运算符语义、提升语义、变量语义等等都整理出来,一下子让它们保持一致。
但是,嘿,我们并不生活在那个完美的世界中。我们生活在一个完美是善的敌人的世界,在 C# 2.0 到 5.0 中你必须说 .Value.
而不是 .
,在 C# 6.0 中说 ?.
。
*
我故意忽略指针类型,它们可以为 null,具有值类型的一些特征和引用类型的一些特征,并且它们有自己的特殊运算符用于取消引用和成员访问。
**
诸如此类的东西也很神奇:可为空的值类型不满足值类型约束、可为空的值类型装箱到空引用或装箱的基础类型,以及许多其他小的特殊行为。但是内存布局并不神奇。它只是布尔值旁边的一个值。
***
函数式程序员当然知道这是maybe monad上的bind操作