【问题标题】:Non-nullable reference types' default values VS non-nullable value types' default values不可空引用类型的默认值 VS 不可空值类型的默认值
【发布时间】:2020-08-26 11:01:10
【问题描述】:

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

以这段代码为例

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 是没有意义的。 为什么选择这个?这与我们一直习惯的不可为空的原则背道而驰。 我认为默认的不可为空字符串的默认值应该是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;

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

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

但是当这样做时:

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

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

考虑到它根本不能防止 null 在不可为空的引用类型变量中出现,看来这个新特性并没有改变任何东西,也根本没有改善开发人员的生活(与非- 可空值类型,不能为空,因此不需要进行空检查)

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

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

谢谢

【问题讨论】:

  • 您忽略了string nonNullableString = default(string); 生成的警告,这就是您看不到任何好处的原因。您将 null 显式存储到不可为空的变量中,因此编译器会抱怨告诉您出了点问题。这就是此功能的好处,尤其是当您将警告视为错误时。
  • 我没有忽略这些警告,事实上我现在的配置是<WarningsAsErrors>CS8600;CS8625</WarningsAsErrors>。我只是对为什么这甚至可能感到非常困惑。我们永远无法确定不可为空的引用类型不会为空,但是对于值类型,我们可以确定...
  • 我在下面回答了您问题的优点部分。顺便说一句,我建议您在此处询问时尝试使用负载较少的语言。您选择的词语(“它没有意义”、“它完全违背了我们一直习惯的(...)原则”)使您的问题读起来更像是咆哮而不是诚实的询问。人们不喜欢被骂,所以如果 Roslyn 团队的真正程序员读到你的帖子,他们可能会少一点“让我们帮助这个人理解”的态度,而多一点“保护自己免受敌意”的态度,这对讨论没有帮助。
  • @JérômeMEVEL 许多“明显”无效/损坏的代码在语法上完全合法,并且不能被检测为损坏......还有很多完全安全和合法的东西 在计算上是不可能证明有效的(参见“停止问题”)。编译器的工作是阻止确定 问题,尝试警告您可能出现的问题,但在您可能正确时不要主动妨碍您。在这种情况下:它给了你一个警告——你还想要什么?
  • @V0ldek 非常感谢您的长篇回答并对我的措辞感到抱歉,英语不是我的母语。只是*咆哮*`不是我的目标,但我想说明为什么这个功能的行为让我很困惑,以便获得不同的意见并更好地理解它

标签: c# c#-8.0 nullable-reference-types non-nullable


【解决方案1】:

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

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

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

至于string nonNullableString = null!;

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

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

所以说到底,唯一的增值似乎只是在签名方面。

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

【讨论】:

  • 谢谢马克。请你解释一下 default(int) 有 0 .. 我明白但为什么 default(string) 为 null 而不是空字符串?
  • @daremachine 再次:defaultnull 的引用类型相同;这只是一个公理/定义的事情; string 是引用类型,因此stringdefaultnull,而不是空字符串。
  • 啊..我不知道字符串是引用类型。这对我来说很有意义。谢谢
  • 我并没有忽略警告,事实上现在我的默认配置是<WarningsAsErrors>CS8600;CS8625</WarningsAsErrors>。我也知道使用null! 是什么意思,因此我从不使用它。这只是说明为什么我对这个功能如此困惑的例子。您能否详细说明您的答案以说明为什么此功能has lots more validity?我不明白我应该如何处理实际上可能为空的不可为空的引用类型。它只是在代码中增加了更多的混乱......
  • @JérômeMEVEL 现实检查:NullReferenceException 是人们在 .NET 中遇到的最常见异常之一; NRT 功能可以在编译时发现可能的 NRT,而不是在运行时命中它。这就是 NRT 的意义和目的。现在,有一些限制 - V0ldek(其他答案)将数组中的空值作为一个很好的例子 - 但到合理的近似值,只要你不习惯性地使用!,并且只要当你阅读并做一些事情它给你的警告:你应该发现你几乎再也看不到NullReferenceException
【解决方案2】:

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

默认值

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

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

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

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

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

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

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

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

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

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

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

string Foo { get; set; }

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

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

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

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

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

bang 运算符 (!) 和 Nullable 值类型

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

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

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

  • HasValue 类型为 bool 的属性
  • Value 类型为 T 的属性 HasValuetrue 的实例称为非空实例。非空实例包含一个已知值,Value 返回该值。

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

int? x = null;

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

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

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

总结

考虑到它根本不能防止 null 在不可为空的引用类型变量中出现,看来这个新特性并没有改变任何东西,也根本没有改善开发人员的生活(相对于非- 可空值类型,不能是 null,因此不需要进行空值检查)

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

来自the official docs on NRTs

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

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

string nonNullableString = default(string);

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-23
    • 1970-01-01
    相关资源
    最近更新 更多