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

Posted

技术标签:

【中文标题】为啥我没有收到关于在 C# 8 中使用结构的类成员可能取消引用 null 的警告?【英文标题】:Why don't I get a warning about possible dereference of a null in C# 8 with a class member of a struct?为什么我没有收到关于在 C# 8 中使用结构的类成员可能取消引用 null 的警告? 【发布时间】:2020-02-13 22:37:08 【问题描述】:

在启用了nullable reference types 的 C# 8 项目中,我有以下代码,我认为它应该给我一个关于可能的 null 取消引用的警告,但没有:

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
    

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

【问题讨论】:

您是否将此作为问题提交到 Roslyn GitHub 存储库? @Dai 我还没有(还);如果这是一个合法的错误而不是我遗漏的东西,我会的。 FWIW,此代码在 C# 7.0 中 not 编译 - 我收到关于缺少构造函数来设置自动属性值的两种类型的错误。不过,它确实可以使用 Roslyn 3.0 和 .NET Core 3.0 编译器进行编译,并且实际上在后两种情况下它都使用 NRE 运行。我正在使用基于 Web 的 IDE,但无法设置编译器选项。 当我将 ExampleStructstruct 更改为 class 时,C# 8.0 编译器会警告我。 @tymtam 用于预览版。在发布版本中,它是Nullable 【参考方案1】:

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

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

我称之为漏洞,因为理论上不可为空的引用值实际上应该始终为非空!呃。 :)

这篇博文似乎解决了这个漏洞:Introducing Nullable Reference Types in C#

避免空值 到目前为止,警告是关于保护可空引用中的空值不被取消引用。硬币的另一面是避免在不可为空的引用中出现空值。

有几种方法可以产生空值,其中大多数值得警告,而其中一些会导致另一个最好避免的“警告海洋”: …

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

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

请注意,这也符合该功能背后更广泛的理念。来自同一篇文章:

所以我们希望它抱怨您现有的代码。但并不讨厌。以下是我们将如何尝试达到这种平衡: …

    无法保证零安全 [强调我的 - PD],即使您对所有警告做出反应并消除了所有警告。分析中有许多漏洞是必然的,也有一些是自愿的。

最后一点:有时警告是“正确”的事情,但会一直触发现有代码,即使它实际上是以空安全方式编写的。在这种情况下,我们会为了方便而不是正确而犯错。我们不能在现有代码上产生“警告海洋”:太多人只会关闭警告而永远不会从中受益。

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

这么多解释为什么事情是这样的。那么问题就变成了,该怎么办呢?这要主观得多,我认为没有正确或错误的答案。也就是说……

我个人会根据具体情况处理我的struct 类型。对于那些 intent 实际上是可为空的引用类型的人,我将应用 ? 注释。否则,我不会。

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

对我来说,这违背了启用可为空引用类型的许多目的。由于struct 类型的此类成员无论如何都需要特殊情况处理,并且由于真正安全处理它同时仍然能够使用不可为空的引用类型的唯一方法是将null 检查您使用struct 的任何地方,我认为接受代码初始化struct 时,这是一个合理的实现选择,代码有责任正确执行此操作并确保不可为空的引用类型成员在fact 初始化为非空值。

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

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

【讨论】:

很好的分析,感谢您找到那篇博文 - 这是一个非常确凿的答案。【参考方案2】:

鉴于 @peter-duniho 的出色回答,似乎从 2019 年 10 月开始,最好将所有非值类型成员标记为可为空的引用。

#nullable enable
public class C

    public int P1  get;  


public struct S

    public C? Member  get;  // Reluctantly mark as nullable reference because
                              // https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/
                              // states:
                              // "Using the default constructor of a struct that has a
                              // field of nonnullable reference type. This one is 
                              // sneaky, since the default constructor (which zeroes 
                              // out the struct) can even be implicitly used in many
                              // places. Probably better not to warn, or else many
                              // existing struct types would be rendered useless."


public class Program

    public static void Main()
    
        var instance = new S();
        Console.WriteLine(instance.Member.P1); // Warning
    

【讨论】:

以上是关于为啥我没有收到关于在 C# 8 中使用结构的类成员可能取消引用 null 的警告?的主要内容,如果未能解决你的问题,请参考以下文章

非常简单的代码中的类、结构或接口成员声明中的标记“while”无效

为啥我收到错误“类型‘PublicTableViewController’没有成员‘handleRefresh’’

c# 曾经面试题

为啥 C# 8.0 中使用声明的变量的闭包不同?

为啥我不能在我的类中内联函数? [复制]

c# 的类成员