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

Why don't I get a warning about possible dereference of a null in C# 8 with a class member of a struct?

在启用了 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 的可空性检测,我应该收到关于这种可能性的编译器警告,但我没有;这是为什么?

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

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

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

此博客文章似乎解决了此漏洞:Introducing Nullable Reference Types in C#

Avoiding nulls So far, the warnings were about protecting nulls in nullable references from being dereferenced. The other side of the coin is to avoid having nulls at all in the nonnullable references.

There are a couple of ways null values can come into existence, and most of them are worth warning about, whereas a couple of them would cause another “sea of warnings” that is better to avoid:

  • 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 [emphasis mine - PD], or else many existing struct types would be rendered useless.

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

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

So we want it to complain about your existing code. But not obnoxiously. Here’s how we are going to try to strike that balance:

  1. There is no guaranteed null safety [emphasis mine - PD], even if you react to and eliminate all the warnings. There are many holes in the analysis by necessity, and also some by choice.

To that last point: Sometimes a warning is the “correct” thing to do, but would fire all the time on existing code, even when it is actually written in a null safe way. In such cases we will err on the side of convenience, not correctness. We cannot be yielding a “sea of warnings” on existing code: too many people would just turn the warnings back off and never benefit from it.

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


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

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

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

对我来说,这违背了启用可空引用类型的很多目的。由于 struct 类型的此类成员无论如何在某些时候都需要特殊情况处理,并且由于真正 安全地 处理它的唯一方法仍然能够使用非空引用类型是在你使用 struct 的任何地方都进行 null 检查,我觉得这是一个合理的实现选择,当代码初始化 struct 时,代码有责任正确地这样做并确保不可为 null 的引用类型成员实际上已初始化为非 null 值。

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

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

鉴于@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
    }
}