resetting/declaring 可为 null 的值类型的 new 与 null

new vs. null for resetting/declaring nullable value types

网上有很多关于如何在 C# 中使用可空值类型的示例。我还注意到,对于如何初始声明值、如何重置它以及在合并时如何使用它(与分配 'default/new' 值有关)似乎有不同的方法:

采用从各种在线资源 (Fiddle) 收集的以下示例:

Console.WriteLine("Initialize with null vs new");
int? b = null;
Console.WriteLine(b.HasValue);

int? c = new int?();
Console.WriteLine(c.HasValue);

Console.WriteLine("Reset as null vs new");
int? d = 123;
d = null;
Console.WriteLine(d.HasValue);

int? e = 123;
e = new int?();
Console.WriteLine(e.HasValue);

Console.WriteLine("Coalesce as null with cast vs new");
int? f = ("foo" == "bar") ? 123 : (int?)null;
Console.WriteLine(f.HasValue);

int? g = ("foo" == "bar") ? 123 : new int?();
Console.WriteLine(g.HasValue);

所有功能都没有问题,但我很想知道这两种方法是否存在技术差异?特别是,在幕后发生了什么不同的事情使得任何方法特别 suitable/unsuitable?

  1. 如果您不使用 null ,则不太清楚如何检查“无值”。检查 if b == null 之类的东西更清晰,更容易,因为大多数人使用该检查来查看是否有值。它可能会在用户期望空变量但在 new 之后不会出现的情况下产生错误。

  2. 调用new不会分配内存,因为这是一个值类型。它将始终采用 int 的大小 + bool 的大小(bool 在那里让我们知道是否使用 hasValue)

    分配了一个值

编译器会将以下所有三种变体翻译成完全相同的 IL:

  • null
  • default
  • new T?()

没有分配,没有额外的内存。运行时和编译器会特殊处理可为 null 的值类型。

我们可以通过使用以下程序并查看翻译后的 c# 代码和 IL(注意 Console.WriteLine 用于确保编译器不会优化掉这些值)看到:

public static class Program {
    public static void Main() {
        int? j = A;
        Console.WriteLine(j);
        j = B;
        Console.WriteLine(j);
        j = C;
        Console.WriteLine(j);
        j = D;
        Console.WriteLine(j);
    }
    public static int? A => 3;
    public static int? B => null;
    public static int? C => default;
    public static int? D => new int?();
}

翻译如下:

public static class Program
{
    public static Nullable<int> A
    {
        get { return 3; } 
    }
    public static Nullable<int> B
    {
        get { return null; }
    }
    public static Nullable<int> C
    {
        get { return null; }
    }
    public static Nullable<int> D
    {
        get { return null; }
    }
    public static void Main()
    {
        Nullable<int> a = A;
        Console.WriteLine(a);
        a = B;
        Console.WriteLine(a);
        a = C;
        Console.WriteLine(a);
        a = D;
        Console.WriteLine(a);
    }
}

现在承认这是一个 反编译的 程序,但是如果你查看 IL,你会看到编译器 returns 每个 [=20] 的值相同=]、CD(仅在此处粘贴 B 的 IL 以保留 space,但请参阅下面的 SharpLab link):

method public hidebysig specialname static 
        valuetype [System.Private.CoreLib]System.Nullable`1<int32> get_B () cil managed 
    {
        .maxstack 1
        .locals init (
            [0] valuetype [System.Private.CoreLib]System.Nullable`1<int32>
        )
        IL_0000: ldloca.s 0
        IL_0002: initobj valuetype [System.Private.CoreLib]System.Nullable`1<int32>
        IL_0008: ldloc.0
        IL_0009: ret
    }

如果你看不到或不知道 IL,有趣的部分是:

initobj valuetype [System.Private.CoreLib]System.Nullable`1<int32>

此默认值初始化结构。编译器和运行时都对可空类型进行特殊处理。 default initialized* 结构是 Nullable<> 表示 null 值的方式。

看到这个 SharpLab demo.**

现在如果我们删除属性并继续分配 null 等会怎样?

public static void Main() {
    int? j = 3;
    Console.WriteLine(j);
    j = null;
    Console.WriteLine(j);
    j = default;
    Console.WriteLine(j);
    j = new int?();
    Console.WriteLine(j);
}

编译器只是将 null 引用“强制转换”到结构类型***。

public static void Main()
{
    Nullable<int> num = 3;
    Console.WriteLine(num);
    Console.WriteLine((Nullable<int>)null);
    Console.WriteLine((Nullable<int>)null);
    Console.WriteLine((Nullable<int>)null);
}

看到这个SharpLab demo

我还想指出,调用 new T?() 并不像其他答案之一所暗示的那样进行分配。 Nullable<> 实际上是一个值类型。正如你在上面看到的 new int?() 默认初始化,所以没有分配。

总之都是一样的。这取决于个人喜好。我个人认为使用 null 是最好和最清晰的选择,我认为你很难找到持不同意见的人。根据上下文,我肯定会双重接管 new T? 并可能超过 default。简单地说:你意味着null,所以使用null

* 默认初始化不设置 HasValueValue 属性,将它们分别保留为 falsedefault,为我们提供 null 表示

** 如果您查看上述 Main 方法的 IL,请不要被 box 指令所迷惑。我们正在调用 Console.Writeline 的重载,它接受 object 从而强制对值类型进行装箱。

*** 众所周知,结构实际上不可能是 null;这同样表明 由运行时和编译器进行特殊处理。