C# 6.0 introduced two new null-propagation operators: ?. and ?[]. They will make null reference check much easier. In this article, we will see how they work and how they implemented internally.
We all know about NullReferenceException and how to avoid it in our code. We just need to check everything for null before accessing some fields\properties\methods.
Null Propagation Operator ?.
var str = GetString();
if (str != null)
{
return str.Length;
}
else
{
return 0;
}
Now we can use Null Propagation operator:
var str = GetString();
return str?.Length ?? 0;
This operator is just a syntax sugar for null reference check. If operand is null, operator returns null and does not execute right part of the expression. If left operand is not null it will return result from the right expression (after .). In our example, it will return Length if str is not null.
Let's look at the other example to see how it work with longer chain:
var str = GetString();
var typeName = str?.Length.GetType().Name;
if str is null, the whole expression after operator will be ignored (Length.GetType().Name is ignored if str is null).
It might be very helpful if you have a chain of methods to invoke and do not want to do a lot of null checks.
var user = repository.GetUser();
var street = user?.Address?.Street;
First it will check if user is null and if not will check if Address is null and then will return Street value. If user is null it will immediately return null and will not perform null check for Address property.
The other question is what is the result type of this operator? In case of str?.Length we will get null if str is null, but Length is of an Integer type. The answer is: Nullable<int>.
If type of the expression is Value Type it will be wrapped in Nullable<> value type.
Let's check it with a code:
var str = "string";
Console.WriteLine((str?.Length.GetType().Name) is string);
// True
var str2 = "string";
Console.WriteLine((str2?.Length) is Nullable<int>);
// True
As I said, this is just a syntax sugar for if/else construction. Let's check one more example and see resulted IL code.
var str = GetString();
Console.WriteLine((str?.Length)?.GetType().Name);
IL code for this:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 74 (0x4a)
.maxstack 1
.locals init ([0] string str,
[1] valuetype [mscorlib]System.Nullable`1<int32> V_1,
[2] valuetype [mscorlib]System.Nullable`1<int32> V_2)
IL_0000: nop
IL_0001: call string ConsoleApplication1.Program::GetString()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: brtrue.s IL_0015
IL_000a: ldloca.s V_2
IL_000c: initobj valuetype [mscorlib]System.Nullable`1<int32>
IL_0012: ldloc.2
IL_0013: br.s IL_0020
IL_0015: ldloc.0
IL_0016: call instance int32 [mscorlib]System.String::get_Length()
IL_001b: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_0020: stloc.1
IL_0021: ldloca.s V_1
IL_0023: call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_0028: brtrue.s IL_002d
IL_002a: ldnull
IL_002b: br.s IL_0043
IL_002d: ldloca.s V_1
IL_002f: call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
IL_0034: box [mscorlib]System.Int32
IL_0039: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
IL_003e: callvirt instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
IL_0043: call void [mscorlib]System.Console::WriteLine(string)
IL_0048: nop
IL_0049: ret
} // end of method Program::Main
Which is more or less the same as following C# code:
var str = GetString();
Nullable<int> result;
if (str != null)
{
var nullable = new Nullable<int>(str.Length);
if (nullable.HasValue)
{
var integer = nullable.GetValueOrDefault();
var length = integer.GetType().Name.Length;
result = length;
}
else
{
result = new int?();
}
}
else
{
result = new int?();
}
Console.WriteLine(result);
Indexer Null Propagation Operator ?[]
Sometimes we need to check if collection is not null and then take some item from the collection:
List<int> list = GetList();
if (list != null)
{
Console.WriteLine(list[1]);
}
Now we can use indexer null propagation operator:
List<int> list = GetList();
Console.WriteLine(list?[0]);
Logic is the same. It will check if list is null, if it is null operator just return null (in our case Nullable<int>), otherwise it will return item with index 0.
This operator has one misunderstanding, if you have list of objects it is hard to see what is null, list or item in list.
Use cases
It is useful to combine null-propagation operator with null-coalescing operator (??). As in first example:
return user?.Address?.Street ?? "Unknown";
The seconds case if null check for delegates. This one:
var handler = SomeEvent;
if (handler != null)
{
handler(new Args());
}
Could be replaced with:
SomeEvent?.Invoke(new Args());