EvilDr
EvilDr

Reputation: 9610

new vs. null for resetting/declaring nullable value types

There are many examples online of how to use nullable value types in C#. I also notice that there seems to be different approaches of how to initially declare the value, how to reset it, and how it use it when coalescing (relating to assigning a 'default/new' value):

Taking the following examples gathered from various online sources (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);

All function without issues, but I'm keen to know if there is a technical difference with either approach? Particularly, is anything different happening under the hood that makes any approach particularly suitable/unsuitable?

Upvotes: 0

Views: 905

Answers (2)

CodeMonkey
CodeMonkey

Reputation: 12444

  1. If you don't use null it's a bit less clear on how to check for "no value". Checking something like if b == null is clearer and easier as most people use that check to see if there's a value or not. It could create bugs where the user expects a null variable but after new it won't be.

  2. Calling new will not allocate memory since this is a value type. It will always take the size of int + size of bool (the bool is there to let us know if a value was assigned or not using hasValue)

Upvotes: 0

pinkfloydx33
pinkfloydx33

Reputation: 12799

The compiler will translate all three of the following variants to the exact same IL:

  • null
  • default
  • new T?()

There's no allocation, no extra memory. The runtime and the compiler treat nullable value types specially.

We can see that by using the following program and looking at the translated c# code and the IL (note Console.WriteLine is used to make sure the compiler doesn't optimize away the values):

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?();
}

This is translated to the following:

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);
    }
}

Now granted this is a decompiled program, but if you look at the IL you will see that the compiler returns the same value for each of B, C, and D (only pasting here the IL for B to preserve space but see the SharpLab link below):

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
    }

If you can't see it or don't know IL, the interesting part is:

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

This default initializes the struct. The compiler and the runtime both have special handling of nullable types. The default initialized* struct is how Nullable<> represents the null value.

See this SharpLab demo.**

Now what if we elided the properties and just kept assigning null, etc.?

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);
}

The compiler just inserts a null reference "cast" to the struct type***.

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);
}

See this SharpLab demo.

I also wanted to call out that calling new T?() does not allocate like one of the other answers suggests. Nullable<> is in fact a value-type. As you see above new int?() default initializes, so no allocation.

In short they are all the same. It comes down to personal preference. Personally I belive the using null is the best and clearest option and I think you'd be hard pressed to find someone with a different opinion. I would definitely do a double-take over new T? and possibly over default depending on the context. Simply put: you mean null, so use null

* Default initialization does not set the HasValue or Value properties, leaving them as false and default respectively, giving us the null representation

** Don't be fooled by the box instructions if you look at the IL for the Main methods above. We are calling an overload of Console.Writeline which accepts an object thus forcing the boxing of the value type.

*** As we all know structs cannot actually be null; this likewise demonstrates that there is special handling by the runtime and compiler.

Upvotes: 2

Related Questions