C#8,无效。我们如何生活

大家好!值得一提的是,我们计划发布Ian Griffiths关于C#8的基本书籍


同时,作者在他的博客上发表了两篇相关文章,其中他考虑了新现象的复杂性,如可空性,无效性和无效性。我们已将这两篇文章翻译为一个标题,并建议对其进行讨论。

C#8.0中最雄心勃勃的新功能称为可空引用

此新功能的目的是消除危险的损害,计算机科学家Tony Hoar曾称其为“ 十亿美元的错误”C#有一个关键字null(在许多其他语言中都可以找到它的对应词),并且该关键字的词根可以追溯到Hoar参与开发的Algol W语言。在这种古老的语言中(出现于1966年),引用某种类型实例的变量可能会收到特殊的含义,这表明该变量现在在任何地方都没有被引用。这个机会被广泛借用,如今许多专家(包括Hoar本人)相信,它已成为有史以来代价高昂的软件错误的最大来源。

假设为零有什么问题?在任何链接都可能指向零的世界中,无论代码中使用了什么链接,您都必须考虑这一点,否则就有可能在运行时被拒绝。有时不太麻烦;如果使用new在声明它的位置的表达式初始化变量,则您知道该变量不等于零。但是,即使是这样一个简单的示例也充满了认知上的负担:在C#8发布之前,编译器无法告诉您是否正在做任何可以将该值转换为的事情null。但是,一旦您开始缝合不同的代码片段,就很难确定这些事情:我现在正在阅读的此属性返回的可能性有多大null?可以传送吗null变成那个方法?在什么情况下可以确定我正在调用的方法是否将此参数设置outnull,而是设置为其他值?而且,此事甚至不仅仅限于记住检查这些东西。尚不完全清楚如果遇到零,应该怎么做。

使用C#中的数字类型时,不会出现这样的问题:如果您编写一个将一些数字作为输入并返回数字的函数,那么您就不必怀疑所传递的值是否真的是数字,并且它们之间是否可以混合使用。调用此类函数时,不必考虑它是否可以返回任何东西而不是数字。除非这样的事件发展使您感兴趣:在这种情况下,您可以声明参数或类型的结果int?,表示在这种特殊情况下,您确实希望允许传输或返回空值。因此,对于数值类型,在更广泛的意义上来说,对于重要类型,零公差一直是自愿选择执行的操作之一。

对于引用类型,在C#8.0之前,零的可允许性不仅是默认设置,而且也不能禁用。

实际上,出于向后兼容的原因,默认情况下,即使在C#8.0中,零有效性也将继续默认运行,因为在您明确请求它们之前,该区域中的新语言功能一直处于禁用状态。

但是,一旦启用此新功能,一切都会改变。激活它的最简单方法是将其添加<Nullablegt;enable</Nullablegt;到元素中。<PropertyGroup>在您的文件中.csproj。 (我注意到,也可以使用更多的花丝控件。如果确实需要,可以将行为配置为null在每一行上分别允许。但是,当我们最近决定在所有项目中都包含此功能时,事实证明它将被大规模激活一次完成一个项目是一项可完成的任务。)

当C#8.0中的允许链接null完全激活时,情况发生了变化:现在,默认情况下,假定仅当您自己未指定相反的链接(与重要类型完全一样)时,这些链接才不允许为null(即使语法是一样的:如果您确实希望整数值是可选的,则可以编写int?,现在编写string ?,如果您要使用字符串引用或null。)

这是非常重要的更改,并且首先,由于其重要性,默认情况下禁用此新功能。 Microsoft可以设计该语言功能的方式有所不同:您可以将默认链接保留为空,并引入新的语法,该语法将允许您指定要确保不允许的语法null。在探索这种可能性时,这也许会降低门槛,但是从长远来看,这种解决方案将是不正确的,因为在实践中,大量C#代码中的大多数链接并非旨在指向null

假定零是一个例外,而不是一条规则,这就是为什么启用此新语言功能后,防止null成为新的默认值的原因。即使在原始功能名称中也反映了这一点:“可空引用”。考虑到链接可能指向nullC#1.0 ,所以这个名称很奇怪。但是开发人员选择强调,现在,空假设进入需要明确请求的事物类别。

C#8.0简化了引入许可链接的过程null,因为它允许您逐步引入此功能。一个人不必做出是或否的选择。这async/await与C#5.0中添加的功能有很大的不同,后者倾向于传播:实际上,异步操作使调用者不得不async,因此,调用此调用方的代码必须async位于堆栈的最顶端,依此类推。幸运的是,允许的类型null构造不同:可以有选择地逐步实现它们。如有必要,您可以逐个或逐行处理文件。

类型允许的最重要方面null(由于简化了向它们的过渡,这是由于默认情况下它们是禁用的)。否则,大多数开发人员将拒绝使用C#8.0,因为这样的转换会在几乎所有代码库中引起警告。但是,出于相同的原因,使用此新功能的入门门槛也相当高:如果一项新功能做出了如此巨大的更改,以至于默认情况下将其禁用,那么您可能不希望将其弄乱,但是切换到该功能存在一些问题总是显得不必要的麻烦。但这很可惜,因为该功能非常有价值。在用户为您完成之前,它有助于发现代码中的错误。

因此,如果您正在考虑引入允许null,请务必注意,您可以逐步介绍此功能。

仅警告

在简单的开/关之后,对整个项目的最粗糙的控制是激活警告而不管注释如何的能力。例如,如果我Corvus.ContentHandling存储库中完全启用了Corvus.ContentHandling.Json的零假设,并将其添加到项目文件中的属性组中,则在其当前状态下,将立即出现来自编译器的20条警告。但是,如果我改用它,只会收到一个警告。<Nullablegt;enable</Nullablegt;<Nullablegt;warnings</Nullablegt;

可是等等!为什么向我显示较少的警告?最后,我只是在这里要求警告。这个问题的答案不是很明显:事实是某些变量和表达式可能是null空的。

无效中性

C#支持两种对无效性的解释。首先,可以将引用类型的任何变量声明为“允许”或“不允许” null,其次,编译器将null逻辑上尽可能地推断该变量是否可以在代码中的任何特定点。本文仅涉及第一种可受理性null即,关于静态类型的变量的(事实上,这不仅适用于变量和参数和字段接近他们在精神;静态和逻辑上推断出受理被null在C#每个表达式确定的。)事实上,受理null在其第一感测,我们正在考虑的是类型系统的扩展。

但是,事实证明,如果我们仅关注某个类型的零可容许性,那么情况将不会像人们可能想到的那样连贯。这不仅是“无效”和“无效”之间的对比null”。实际上,还有两种可能性。有一个“未知”类别,由于具有通用名称,因此该类别为必填项。如果您有一个无限制的类型参数,则将无法找到有关其有效性的任何信息null:使用适当的通用方法或类型的代码可以替换其中的参数,无论是允许还是不允许null。您可以添加限制,但是通常这样的限制是不可取的,因为它们会缩小广义类型或方法的范围。因此,对于某些无限制类型参数的变量或表达式T,零(非)有效性必须是未知的;也许在每种情况下,可否受理问题null将会为他们单独决定,但是我们不知道哪个选项会出现在通用代码中,因为它取决于类型参数。

后一类称为“中立”。根据“中立性”的原则,一切都在C#8.0出现之前就起作用了,如果您不激活使用可为空的链接的功能,则这将起作用。 (基本上,这是追溯性的一个例子。尽管在激活引用的空有效性之前,空中性的想法最初是在C#8.0中作为代码的自然状态引入的,但C#设计人员坚持认为此属性永远不会真正与C#无关)。

在这种情况下,也许您不必解释“中立”的含义,因为C#一直在这种情况下起作用,所以您自己可以理解所有内容……尽管,这也许有点不诚实。因此,请听:在一个众所周知的可接收性世界中nullnull-eutral表达式的最重要特征是它们不会引起有关null可接受性的警告。您可以将零位中性表达式指定为允许null但不允许的变量。Null-中性变量(以及属性,字段等),可以分配编译器认为“可能null”或“不null”的表达式

因此,如果您仅打开警告,则不会有很多新的警报。所有代码都保留在禁用的有效性批注的上下文中null,因此所有变量,参数,字段和属性都是null中性的,这意味着如果您尝试将它们与考虑到的任何实体结合使用,则不会收到任何警告null

那么,为什么我会收到警告?一个常见的原因是因为试图以一种无法接受的方式结交朋友,而这两点代码都被考虑在内null。例如,假设我有一个库,其中完全包含了允许链接null,并且该库具有以下深加工的类:

public static class NullableAwareClass
	{
	    public static string? GetNullable() => DateTime.Now.Hour > 12 ? null : "morning";
	

	    public static int RequireNonNull(string s) => s.Length;
	}

此外,在另一个项目中,我可以在激活了空有效性警告但禁用了相应注释的上下文中编写此代码:

static int UseString(string x) => NullableAwareClass.RequireNonNull(x);

由于禁用了有关无效有效性的注释,因此x此处的参数null中性。这意味着编译器无法确定此代码是否正确。如果在null中性表达式与考虑在内的表达式混合使用的情况下,编译器发出警告,null这些警告中有很大一部分可被视为可疑-因此,不会发出警告。

有了这个包装器,我实际上隐藏了代码考虑到有效性的事实null这意味着现在我可以这样写:

	int x = UseString(NullableAwareClass.GetNullable());

编译器知道它GetNullable可以返回什么null,但是由于我使用null中性参数调用了该方法,因此程序不知道这是对还是错。使用null-neutral包装器,我使编译器撤防,现在在这里看不到问题。但是,如果直接将这两种方法结合起来,一切都会有所不同:

int y = NullableAwareClass.RequireNonNull(NullableAwareClass.GetNullable());

在这里,我将结果传递GetNullableRequireNonNull。如果我尝试在启用空假设的上下文中执行此操作,则无论我打开还是关闭相应注释的上下文,编译器都会生成警告。在这种特殊情况下,注释的上下文无关紧要,因为没有引用类型的声明。如果启用有关假定为null的警告,但禁用相应的批注,则所有声明都将变为null中性,但这并不意味着所有表达式都变为中性。因此,我们知道结果GetNullable为空。因此,我们得到警告。

总结:由于在允许的禁用注释的上下文中的所有声明null都是null-中立,我们不会收到很多警告,因为大多数表情都是- null中立的。但是,null当表达式不通过任何零位中性中介时,编译器仍将能够捕获与假设有关的错误。此外,在这种情况下,最大的好处将是通过检测与尝试取消引用潜在空值相关的错误.,例如:

int z = NullableAwareClass.GetNullable().Length;

如果您的代码设计合理,则应该不会出现大量此类错误。

整个项目的逐步批注

迈出第一步-激活警告,然后逐个文件逐步进行批注。将它们立即包含在整个项目中很方便,查看出现警告的文件-然后选择警告相对较少的文件。再次,在整个项目级别将其关闭,然后将其写入所选文件的顶部#nullable enable。这将完全启用null整个文件中的假设(包括警告和注释)(除非您使用另一个指令再次将其关闭)#nullable)然后,您可以遍历整个文件,并确保将所有可能为空的实体都注释为允许null(即add ?),然后处理该文件中的警告(如果有的话)。

事实证明,添加所有必要的注释是消除所有警告所需要的。反之亦有可能:您可能会注意到,当您对一个文件进行整齐的注释时,null,使用它的其他文件中也出现了其他警告。通常,此类警告并不多,您有时间快速修复它们。但是,如果由于某些原因在此步骤之后您只是淹没在警告中,那么您仍然可以采用几种解决方案。首先,您可以简单地取消选择,保留此文件,然后再进行另一个选择。其次,您可以有选择地关闭那些您认为引起最多问题的成员的注释。 (#nullable您可以根据需要使用该指令多次,因此,如果需要,甚至可以逐行控制null有效性设置。)也许如果稍后在大多数项目中已经激活了null有效性时返回此文件,将会看到较少的内容。警告比第一次。

有时候,无法以这种简单的方式解决问题。因此,在与序列化相关的某些情况下(例如,当使用Json.NET或Entity Framework时),工作可能会更加困难。我认为这个问题值得一文。

与假设的链接null提高了代码的表达能力,并增加了编译器在用户遇到错误之前捕获错误的机会。因此,最好尽可能包含此功能。而且,如果有选择地包括它,那么它的好处将开始变得更快。

All Articles