$ dotnet run33Unhandled exception.NullReferenceExceptionRoslynheap"Ada"string1string? name = null;2int? age = null;3var n = name?.Length ?? 0;4age ??= 30;5name = "Ada";6n = name.Length;7Console.WriteLine(n + age);variablesname · string?nullage · int?·HasValue falseHasValue truen · int·nullstring?030"Ada"refok333CS8602
Front: Program.cs and the variables name, age and nBack: the console, Roslyn and the heapTokens: values (null, 0, 30, 3, 33) moving between code and variablesFront: Program.cs and the three variables it uses: name (string?), age (int?, with its HasValue flag) and n (int). Back: the console, the compiler Roslyn, which tracks which variables may be null, and the heap. Values travel from code lines into the variables: null, then 0 through ?. and ??, 30 through ??=, a reference to "Ada", 3, and finally 33 on the console. The last step shows what happens without line 5. The arrow → "Ada" stands for the reference: the object's address. Safe C# code never sees the address itself.

Null and ?

  1. A variable of a reference type, such as string, holds a reference: the address of an object on the heap. null is the reference to no object at all. Line 1 sets name to null.

The null operators

Null safety

What happens

// step 1/9 · null
line 1   string? name = null;
string   a reference type: the variable holds an address
null     the reference to no object
name     null

The program is real C#. With nullable reference types on (the default for new .NET 6+ projects), it builds with no warnings and prints 33. The variables row shows what the three local variables hold. Where the runtime keeps locals (stack slots or CPU registers) is its own choice.