为什么我没有收到有关在C#8中使用结构的类成员可能会取消引用null的警告?

Dyl*_*nSp 6 c# nullable c#-8.0

在启用了可空引用类型的C#8项目中,我有以下代码,我认为应该向我发出有关可能的空取消引用的警告,但不会:

public class ExampleClassMember
{
    public int Value { get; }
}

public struct ExampleStruct
{
    public ExampleClassMember Member { get; }
}

public class Program
{
    public static void Main(string[] args)
    {
        var instance = new ExampleStruct();
        Console.WriteLine(instance.Member.Value);  // expected warning here about possible null dereference
    }
}
Run Code Online (Sandbox Code Playgroud)

instance使用默认的构造函数初始化,instance.Member设置为默认值ExampleClassMember,这是null。这样,instance.Member.ValueNullReferenceException在运行时抛出一个。据我了解C#8的可空性检测,我应该收到关于这种可能性的编译器警告,但我没有。这是为什么?

Pet*_*iho 11

请注意,对的调用没有理由发出警告Console.WriteLine()。引用类型属性不是可为null的类型,因此编译器无需警告它可能为null。

您可能会争辩说,编译器应就struct本身的引用发出警告。在我看来,这是合理的。但是,事实并非如此。这似乎是一个漏洞,是由值类型的默认初始化引起的,即,必须始终有一个默认(无参数)构造函数,该构造函数总是始终将所有字段清零(对于引用类型字段为空,对于数字类型为零,等等)。 )。

我称其为漏洞,因为从理论上说,不可为空的参考值实际上应该始终为非!咄。:)

此漏洞似乎在以下博客文章中得到了解决:在C#中引入可空引用类型

避免空值到目前为止,警告是关于防止可空引用中的空值被取消引用。硬币的另一面是要避免在不可为空的引用中完全没有为空。

可以通过以下几种方式来生成空值,其中大多数值得警告,而其中的一些会导致另一个“警告之海”,最好避免:

  • 使用具有非空引用类型字段的结构的默认构造函数。这是一个偷偷摸摸的工作,因为默认构造函数(将结构归零)甚至可以在很多地方隐式使用。最好不要警告 [emphasis mine-PD],否则许多现有的struct类型将变得无用。

换句话说,是的,这是一个漏洞,但是,不是漏洞。语言设计者已经意识到了这一点,但是选择将这种情况排除在警告之外,因为如果采用struct初始化的工作方式,否则这样做是不切实际的。

请注意,这也符合该功能背后的广泛哲学。在同一篇文章中:

因此,我们希望它抱怨您现有的代码。但并非令人讨厌。这就是我们要设法达到这种平衡的方法:

  1. 有没有保证零安全 [重点煤矿- PD] ,即使你反应,并消除所有的警告。分析中有许多漏洞是必要的,也有一些漏洞可以选择。

到最后一点:有时警告是“正确的”事情,但即使实际上以null安全的方式编写,警告也会一直在现有代码上触发。在这种情况下,我们会偏向于方便,而不是正确。我们不能在现有代码上产生“警告之海”:太多的人只会关闭警告而从中受益匪浅。

另请注意,名义上不可为空的引用类型(例如string[])的数组也存在相同的问题。创建数组时,所有参考值均为null,但这是合法的,不会生成任何警告。


这么多的解释为什么事情就是这样。然后问题变成了,该怎么办?这要主观得多,我不认为答案是对还是错。那说...

我个人会struct逐案对待我的类型。对于那些意图实际上是可为空的引用类型的人,我将应用?注释。否则,我不会。

从技术上讲,中的每个单个引用值struct都应为“可为空”,即?在类型名称中包含可为空的注释。但是,与许多类似功能(例如C#或constC ++中的async / await )一样,它具有“感染性”的方面,因为您稍后需要覆盖该批注(带有!批注),或者包括显式的null检查,或仅将该值分配给另一个可为空的引用类型变量。

对我来说,这使启用可为空的引用类型的许多目的都无法实现。由于这类struct类型的成员无论如何在某些时候都需要特殊情况处理,并且既然真正能够安全地处理它的唯一方法同时仍能够使用非空引用类型,那就是在使用的任何地方都放置null检查struct,我觉得接受这样的选择是合理的实现选择:代码初始化时struct,代码有责任正确地这样做,并确保实际上将非空引用类型成员初始化为非空值。

这可以通过提供一种“官方的”初始化方法来辅助,例如非默认构造函数(即带有参数的构造函数)或工厂方法。仍然总是有使用默认构造函数的风险,或者根本没有使用任何构造函数的风险(如在数组分配中一样),但是通过提供一种方便的方法来struct正确地初始化正确的构造函数,这将鼓励使用该构造函数的代码避免在非可为空的变量。

就是说,如果您要针对可为空的引用类型提供100%的安全性,那么显然针对该特定目标的正确方法是始终对structwith中的每个引用类型成员进行注释?。这意味着每个字段和每个自动实现的属性,以及直接返回此类值或此类值的乘积的任何方法或属性获取器。然后,在将此类值复制到不可为空的变量的每个点上,使用方代码将需要包括空检查或允许空的运算符。