Reputation: 9610
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
Reputation: 12444
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.
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
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