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

Jér*_*VEL 6 c# non-nullable c#-8.0 nullable-reference-types

这不是我关于可为空引用类型的第一个问题,因为我已经使用它几个月了。但是我体验的越多,我就越困惑,我看到该功能增加的价值就越少。

以这个代码为例

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

int? nullableInt = default(int?);
int nonNullableInt = default(int);
Run Code Online (Sandbox Code Playgroud)

执行给出:

nullableString => null

nonNullableString => null

nullableInt => null

nonNullableInt => 0

(不可为空的)整数的默认值一直是,0但对我来说,不可为空的字符串的默认值是null. 为什么要做出这个选择?这与我们一直习惯的不可为空原则相反。我认为默认的不可为空字符串的默认值应该是String.Empty.

我的意思是在 C# 实现的深处,它必须被指定0int. 我们也可以选择12不,共识是0。那么我们不能在 Nullable 引用类型功能被激活时只指定 a stringis的默认值String.Empty吗?此外,微软似乎希望在不久的将来在 .NET 5 项目中默认激活它,因此此功能将成为正常行为。

现在与对象相同的示例:

Foo? nullableFoo = default(Foo?);
Foo nonNullableFoo = default(Foo);
Run Code Online (Sandbox Code Playgroud)

这给出:

nullableFoo => null

nonNullableFoo => null

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

现在进一步扩展这个问题

string nonNullableString = null;
int nonNullableInt = null;
Run Code Online (Sandbox Code Playgroud)

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

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

但是,这样做时:

string nonNullableString = null!;
int nonNullableInt = null!;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

谢谢

Mar*_*ell 8

同样,这对我来说没有意义,在我看来, Foo 的默认值应该是 new Foo() (如果没有可用的无参数构造函数,则会出现编译错误)

这是一个意见,但是:这不是它的实施方式。default表示null引用类型,即使根据可空性规则它是无效的。编译器发现这一点并在线上警告你Foo nonNullableFoo = default(Foo);

警告 CS8600 将空文字或可能的空值转换为不可为空的类型。

至于string nonNullableString = null!;

编译器对第一行完全没问题,根本没有警告。

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

所以最后似乎唯一增加的价值只是在签名方面。

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

  • @JérômeMEVEL 现实检查:“NullReferenceException”是人们在 .NET 中遇到的“最常见的”异常之一;NRT 功能可以在编译时发现可能的 NRT,而不是在运行时命中它。这就是 NRT 的“要点和目的”。现在,有一些限制 - V0ldek(其他答案)将数组中的空值作为一个很好的例子 - 但是*合理的近似*,只要你不习惯使用“!”,并且只要你阅读*和对它给你的警告做一些事情:你“应该”发现你几乎再也看不到任何“NullReferenceException”了 (2认同)

V0l*_*dek 5

您对编程语言设计的工作方式感到非常困惑。

默认值

(不可为空的)整数的默认值一直是,0但对我来说,不可为空的字符串的默认值是null. 为什么要做出这个选择?这完全违背了我们一直习惯的不可为空的原则。我认为默认的 non-nullablestring的默认值应该是String.Empty.

变量的默认值从一开始就是 C# 语言的基本特性。规范定义了默认值

对于value_type的变量,默认值与 value_type 的默认构造函数([参见] 默认构造函数)计算的值相同。对于reference_type的变量,默认值为null

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

默认值的初始化通常是通过让内存管理器或垃圾收集器在分配使用之前将内存初始化为所有位为零来完成的。出于这个原因,使用所有位为零来表示空引用是很方便的。

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

我认为无法为引用类型指定默认值与无法在值类型上定义无参数构造函数的情况类似 - 运行时需要一个快速的全零默认null值,而值是引用类型的合理默认值. 并非所有类型都具有合理的默认值——什么是 a 的合理默认值TcpClient

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

我的意思是在 C# 实现的深处,它必须被指定0int. 我们也可以选择12不,共识是0。那么我们不能在 Nullable 引用类型功能被激活时只指定 a stringis的默认值String.Empty吗?

正如我所说,归根结底是将一系列内存归零是非常快速和方便的。没有运行时组件负责检查给定类型的默认值是什么,并在创建新类型时在数组中重复该值,因为这会非常低效。

您的提议基本上意味着运行时必须以某种方式string在运行时检查s的可空性元数据,并将全零的不可空string值视为空值string。这将是一个非常复杂的更改,仅针对空string. 仅使用静态分析器在您分配null而不是合理的默认值时向您发出警告会更具成本效益string。幸运的是,我们有这样的分析器,即 NRT 功能,它始终拒绝编译包含如下定义的类:

string Foo { get; set; }
Run Code Online (Sandbox Code Playgroud)

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

string Foo { get; set; } = "";
Run Code Online (Sandbox Code Playgroud)

(顺便说一下,我建议打开将警告视为错误,但这是一个品味问题。)

同样,这对我来说没有意义,在我看来, Foo 的默认值应该是 new Foo() (如果没有可用的无参数构造函数,则会出现编译错误)。为什么默认情况下将不应该为空的对象设置为空?

这将使您无法在没有默认构造函数的情况下声明引用类型的数组。大多数基本集合使用数组作为底层存储,包括List<T>. N每当您创建 size 的数组时,它都会要求您分配类型的默认实例N,这又是非常低效的。构造函数也可能有副作用。我不打算进一步思考这会破坏多少东西,我只想说这不是一个容易的改变。考虑到 NRT 无论如何实施是多么复杂(仅NullableReferenceTypesTests.csRoslyn 存储库中的文件就有大约 130,000 行代码),引入这种更改的成本效益......不是很好。

bang 运算符 (!) 和可空值类型

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

null值仅对引用类型和可为空值类型有效。可空类型再次在规范中定义:

可空类型可以表示其基础类型的所有值加上一个null 附加值。写了一个可为空的类型T?,其中T是基础类型。此语法是 的简写System.Nullable<T>,这两种形式可以互换使用。(...) 可为空类型的实例T?具有两个公共只读属性:

  • 一个HasValue类型的属性bool
  • Value类型的属性T 的实例为其中HasValuetrue被说成是非空。非空实例包含一个已知值并Value返回该值。

可以为其不分配的原因nullint是相当明显的-int是一个值类型且需要32位和表示整数。该null值是一个特殊的引用值,它是机器字大小并表示内存中的一个位置。分配nullint没有合理的语义。Nullable<T>存在专门用于允许null赋值给值类型以表示“无值”场景。但请注意,做

int? x = null;
Run Code Online (Sandbox Code Playgroud)

纯粹是语法糖。的全零值Nullable<T>是“没有价值”的情况,因为这意味着HasValuefalse。在null任何地方都没有分配魔法值,就像说一样= default——它只是创建一个给定类型的新的全零结构T并分配它。

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

bang 运算符专门用于告诉编译器“我知道我正在取消引用可空值/将空值分配给不可空值,但我比你更聪明,我知道这不会破坏任何东西” . 它禁用静态分析警告。但它不会神奇地扩展底层类型以适应null值。

概括

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

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

来自NRT 的官方文档

与无法从变量声明中确定设计意图的早期 C# 版本中处理引用变量相比,此新功能具有显着优势。编译器没有提供针对引用类型 (...) 的空引用异常的安全性。这些警告是在编译时发出的。编译器不会在可为空的上下文中添加任何空检查或其他运行时构造。在运行时,可空引用和不可空引用是等效的。

所以你是对的,“唯一增加的价值只是在签名方面”静态分析,这就是我们首先拥有签名的原因。这不是对开发人员生活的改善吗?请注意,您的线路

string nonNullableString = default(string);
Run Code Online (Sandbox Code Playgroud)

发出警告。如果您没有忽略它(或者甚至更好,打开将警告视为错误),您将获得价值 - 编译器为您发现了代码中的错误。

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