不可空引用类型的默认值 VS 不可空值类型的默认值

Non-nullable reference types' default values VS non-nullable value types' default values

这不是我遇到的第一个关于可为 null 的引用类型的问题,因为我已经经历了几个月。但我体验得越多,我就越困惑,也就越看不到该功能带来的附加价值。

以此代码为例

string? nullableString = default(string?);
string nonNullableString = default(string);

int? nullableInt = default(int?);
int nonNullableInt = default(int);

执行得到:

nullableString => null

nonNullableString => null

nullableInt => null

nonNullableInt => 0

(不可为空的)整数的默认值一直是 0 但是 对我来说,不可为 null 的字符串的默认值是 null 没有意义。 为什么这个选择?这与我们一直习惯的不可为空原则背道而驰。 我认为默认的不可空字符串的默认值应该是 String.Empty.

我的意思是在 C# 实现的深处,必须指定 0int 的默认值。我们也可以选择 12 但不,共识是 0。那么我们是不是可以在Nullable引用类型特性激活的时候,直接指定一个string的默认值为String.Empty呢?此外,微软似乎希望在不久的将来通过 .NET 5 项目默认激活它,因此该功能将成为 正常 行为。

现在使用对象的相同示例:

Foo? nullableFoo = default(Foo?);
Foo nonNullableFoo = default(Foo);

这给出:

nullableFoo => null

nonNullableFoo => null

同样这对我来说没有意义,在我看来 Foo 的默认值应该是 new Foo() (或者如果没有可用的无参数构造函数则给出编译错误)。 为什么默认设置为 null 一个不应为 null 的对象?

现在进一步扩展这个问题

string nonNullableString = null;
int nonNullableInt = null;

编译器对第 1 行给出了警告,可以通过我们 .csproj 文件中的简单配置将其转换为错误:<WarningsAsErrors>CS8600</WarningsAsErrors>。 并且它按预期给出了第二行的编译错误。

所以不可为 null 的值类型和不可为 null 的引用类型之间的行为是不一样的,但这是 acceptable 因为我可以覆盖它。

但是这样做时:

string nonNullableString = null!;
int nonNullableInt = null!;

第一行编译器完全没问题,根本没有警告。 我最近在体验可空引用类型功能时发现 null!,我希望编译器也适用于第二行,但事实并非如此。现在我真的很困惑为什么微软决定实施不同的行为。

考虑到它根本无法防止在不可空引用类型变量中使用 null,看来这个新功能不会改变任何东西,也不会改善开发人员的生活(与不能为 null 的不可空值类型相反,因此不需要进行空值检查)

所以最后似乎唯一增加的价值只是在签名方面。开发人员现在可以明确方法的 return 值是否可以为空,或者 属性 是否可以为空(例如,在数据库 table 的 C# 表示中NULL 是列中允许的值。

除此之外,我不知道如何有效地使用这个新功能,能否请您给我其他有关如何使用可为空引用类型的有用示例? 我真的很想好好利用这个功能来改善我的开发人员的生活,但我真的不知道如何...

谢谢

Again this doesn't make sense to me, in my opinion the default value of a Foo should be new Foo() (or gives a compile error if no parameterless constructor is available)

这是一个意见,但是:这不是它的实施方式。 default 对于 reference-types 意味着 null,即使根据可空性规则它是无效的。编译器会发现这一点并在行 Foo nonNullableFoo = default(Foo);:

上警告您

Warning CS8600 Converting null literal or possible null value to non-nullable type.

至于string nonNullableString = null!;

The compiler is completely fine with the 1st line, no warning at all.

告诉它忽略它;这就是 ! 的意思 。如果你告诉编译器不要抱怨某事,抱怨它没有抱怨是无效的。

So at the end it seems the only value added is just in terms of signatures.

不,它有更多的有效性,但是如果你忽略它 确实 引发的警告(CS8600 以上),并且如果你 抑制 它为您做的其他事情 (!):是的,它的用处不大。所以...不要那样做?

您对编程语言设计的工作原理感到很困惑。

默认值

The default value of an (non-nullable) integer has always been 0 but to me it doesn't make sense a non-nullable string's default value is null. Why this choice? This is completely against non-nullable principles we've always been used to. I think the default non-nullable string's default value should have been String.Empty.

变量的默认值是 C# 语言从一开始就存在的基本特征。 The specification defines the default values:

For a variable of a value_type, the default value is the same as the value computed by the value_type's default constructor ([see] Default constructors). For a variable of a reference_type, the default value is null.

从实际的角度来看,这是有道理的,因为默认值的基本用法之一是在声明给定类型的新值数组时。由于这个定义,运行时可以将分配数组中的所有位清零 - 值类型的默认构造函数始终是所有字段中的 all-zero 值,并且 null 表示为 all-zero 引用.这实际上是规范中的下一行:

Initialization to default values is typically done by having the memory manager or garbage collector initialize memory to all-bits-zero before it is allocated for use. For this reason, it is convenient to use all-bits-zero to represent the null reference.

现在,可空引用类型功能 (NRT) 已于去年随 C#8 一起发布。这里的选择不是“尽管有 NRT,让我们将默认值实现为 null”,而是“让我们不要浪费时间和资源来尝试完全修改 default 关键字的工作方式,因为我们正在引入 NRT ”。 NRT 是程序员的注解,根据设计,它们对运行时的影响为零

我认为无法为引用类型指定默认值与无法在值类型上定义无参数构造函数类似 - 运行时需要快速 all-zero 默认值和 null 值是引用类型的合理默认值。并非所有类型都有合理的默认值 - TcpClient 的合理默认值是多少?

如果您想要自己的自定义默认值,请实施静态 Default 方法或 属性 并将其记录下来,以便开发人员可以将其用作该类型的默认值。无需更改语言的基础知识。

I mean somewhere deep down in the implementation of C# it must be specified that 0 is the default value of an int. We also could have chosen 1 or 2 but no, the consensus is 0. So can't we just specify the default value of a string is String.Empty when the Nullable reference type feature is activated?

正如我所说,归零内存范围非常快速和方便。没有运行时组件负责检查给定类型的默认值是什么,并在创建新类型时在数组中重复该值,因为那样效率非常低。

你的提议基本上意味着运行时必须在运行时以某种方式检查 strings 的可空性元数据并将 all-zero non-nullable string 值视为一个空 string。这将是一个非常复杂的变化,深入挖掘运行时只是为了这个空 string 的特殊情况。当您分配 null 而不是将合理的默认值分配给 non-nullable string 时,仅使用静态分析器来警告您更多 cost-efficient。幸运的是我们有这样的分析器,即 NRT 功能,它始终拒绝编译我的 类 包含这样的定义:

string Foo { get; set; }

通过发出警告并强迫我将其更改为:

string Foo { get; set; } = "";

(顺便说一句,我建议打开将警告视为错误,但这是个人喜好问题。)

Again this doesn't make sense to me, in my opinion the default value of a Foo should be new Foo() (or gives a compile error if no parameterless constructor is available). Why by default setting to null an object that isn't supposed to be null?

除其他外,这会使您无法在没有默认构造函数的情况下声明引用类型的数组。大多数基础集合使用数组作为底层存储,包括List<T>。每当创建大小为 N 的数组时,它都会要求您分配 N 个类型的默认实例,这又是非常低效的。构造函数也可能有副作用。我不会进一步考虑这会破坏多少东西,我只想说这不是一个容易做出的改变。考虑到无论如何实施 NRT 是多么复杂(Roslyn 存储库中的 NullableReferenceTypesTests.cs 文件单独就有 ~130,000 行代码 ),cost-efficiency 引入这样的更改是...不太好。

bang 运算符 (!) 和可为 Null 的值类型

The compiler is completely fine with the 1st line, no warning at all. I discovered null! recently when experiencing with the nullable reference type feature and I was expecting the compiler to be fine for the 2nd line too but this isn't the case. Now I'm just really confused as for why Microsoft decided to implement different behaviors.

null 值仅对引用类型和可空值类型有效。可空类型再次定义为 in the spec:

A nullable type can represent all values of its underlying type plus an additional null value. A nullable type is written T?, where T is the underlying type. This syntax is shorthand for System.Nullable<T>, and the two forms can be used interchangeably. (...) An instance of a nullable type T? has two public read-only properties:

  • A HasValue property of type bool
  • A Value property of type T An instance for which HasValue is true is said to be non-null. A non-null instance contains a known value and Value returns that value.

您不能将 null 分配给 int 的原因很明显 - int 是一种采用 32 位并表示整数的值类型。 null 值是一个特殊的参考值,大小为 machine-word,代表内存中的一个位置。将 null 分配给 int 没有合理的语义。 Nullable<T> 的存在专门用于允许 null 赋值给值类型以表示“无值”场景。但请注意做

int? x = null;

纯粹是语法糖。 Nullable<T> 的 all-zero 值是“无值”场景,因为它意味着 HasValuefalse。没有神奇的 null 值被分配到任何地方,它与说 = default 相同——它只是创建一个给定类型 T 的新 all-zero 结构并分配它。

所以,答案再次是——没有人故意将其设计为与 NRT 不兼容。自从在 C#2 中引入以来,可为 Null 的值类型是该语言的一个更基本的功能,它的工作方式与此类似。而且您建议它工作的方式并没有转化为明智的实现 - 您是否希望所有值类型都可以为空?然后他们都必须有 HasValue 字段需要一个额外的字节并且可能会搞砸填充(我认为将 ints 表示为 40 位类型而不是 32 位类型的语言将被视为异端 :))。

bang 运算符专门用于告诉编译器“我知道我正在将 nullable/assigning null 解引用到 non-nullable,但我比你聪明,我知道事实上,这不会破坏任何东西”。它禁用静态分析警告。但它不会神奇地扩展基础类型以适应 null 值。

总结

Considering it doesn't protect at all against having null in a non-nullable reference type variable, it seems this new feature doesn't change anything and doesn't improve developers' life at all (as opposed to non-nullable value types which could NOT be null and therefore don't need to be null-checked)

So at the end it seems the only value added is just in terms of signatures. Developers can now be explicit whether or not a method's return value could be null or not or if a property could be null or not (for example in a C# representation of a database table where NULL is an allowed value in a column).

来自 the official docs on NRTs:

This new feature provides significant benefits over the handling of reference variables in earlier versions of C# where the design intent can't be determined from the variable declaration. The compiler didn't provide safety against null reference exceptions for reference types (...) These warnings are emitted at compile time. The compiler doesn't add any null checks or other runtime constructs in a nullable context. At runtime, a nullable reference and a non-nullable reference are equivalent.

所以您说得对,“唯一增加的价值就是签名”静态分析,这就是我们首先拥有签名的原因。那是不是对开发人员生活的改善?请注意,您的行

string nonNullableString = default(string);

发出警告。如果您没有忽略它(或者更好的是,启用了将警告视为错误),您将获得价值 - 编译器为您在您的代码中发现了一个错误。

它能防止您在运行时将 null 分配给 non-null 引用类型吗?不会。它会改善开发人员的生活吗?千次是的。该功能的强大功能来自编译时完成的警告和可空分析。如果您忽略 NRT 发出的警告,后果自负。您可以忽略编译器的帮助这一事实并不意味着它毫无用处。毕竟您也可以将整个代码放在 unsafe 上下文中并用 C 编写程序,这并不意味着 C# 没用,因为您可以绕过它的安全保证。