Null-Conditional and Null-Coalescing

The ?., ?[], ?? and ??= operators, each shown beside the if statement it replaces, plus C# 14 null-conditional assignment.

Syntaxa?.Member a?[index] a ?? fallback a ??= fallback

A reference variable can hold null, which means it points at no object. Reading a member of null throws NullReferenceException. These operators deal with that case without wrapping every line in an if statement.

?. and ?[]

a?.Member reads the member only when a is not null; otherwise the whole expression is null. a?[0] does the same for an index. They short-circuit, so in a?.B.C() nothing to the right runs when a is null. Brackets break that: (a?.B).C() throws again. When the member is a value type, the result is the nullable version, so text?.Length is an int?.

?? and ??=

a ?? b is a when a is not null and b otherwise, and b is only evaluated when it is needed. a ??= b assigns b to a only while a is null; it is exactly if (a == null) { a = b; }. Both are right-associative, so a ?? b ?? c means a ?? (b ?? c) and yields the first non-null value.

New in C# 14

C# 14 allows ?. and ?[] on the left of an assignment to a reference type. customer?.Order = order; assigns, and evaluates the right side, only when customer is not null. Compound forms such as += are allowed; ++ and -- are not. Earlier versions needed the if statement.

Example

Example · csharp
using System.Text;

string? middleName = null;

// ?. reads the member only when the left side is not null.
// When the left side is null, the whole expression is null.
int? shorthand = middleName?.Length;

// The same thing written out longhand.
int? longhand;
if (middleName == null)
{
    longhand = null;
}
else
{
    longhand = middleName.Length;
}
Console.WriteLine(shorthand == longhand);

// ?[] is the same idea for an index. A string is indexed by character.
char? initial = middleName?[0];
Console.WriteLine(initial == null);

// ?? supplies a value to use when the left side is null.
string display = middleName ?? "(none)";
Console.WriteLine(display);

// ??= assigns only while the variable is still null.
string? nickname = null;
nickname ??= "Ada";     // assigns
nickname ??= "Grace";   // skipped: nickname is no longer null
Console.WriteLine(nickname);

// Output:
// True
// True
// (none)
// Ada

When to use it

  • Read a value out of an optional object without an if statement around it.
  • Fall back to a default setting when the configured one is missing.
  • Create a list or buffer on first use with one ??= line.
  • Give a display string for a record whose optional field has not been filled in.

More examples

First value that is not null

This is how settings are layered: command line first, then environment, then a built-in default. Note the type of logLevel. The compiler knows the chain ends in a non-null string, so the result is string rather than string?, and the rest of the code does not have to keep checking. The second half is the everyday use of ?? to get an int out of an int?.

Example · csharp
string? fromArgument = null;
string? fromEnvironment = "debug";
string fallback = "info";

// Right-associative: fromArgument ?? (fromEnvironment ?? fallback).
// The first non-null value wins and the rest are not evaluated.
string logLevel = fromArgument ?? fromEnvironment ?? fallback;
Console.WriteLine(logLevel);

// ?? also turns a nullable number into a plain one.
int? configuredRetries = null;
int retries = configuredRetries ?? 3;
Console.WriteLine(retries);

// Output:
// debug
// 3

A chain, and the brackets that break it

You only need ?. on the part that can actually be null; Trim and ToUpperInvariant return a real string, so a second ?. would add noise. The commented line is worth remembering because it looks harmless: wrapping any part of the chain in brackets ends the short-circuit, and the member access after the bracket runs on null.

Example · csharp
string? text = null;

// One ?. protects the whole chain to its right.
Console.WriteLine(text?.Trim().ToUpperInvariant() ?? "(empty)");

// Brackets cut the chain in two. The bracketed part is null, and .Length on
// null throws NullReferenceException, so this line would be a bug:
// Console.WriteLine((text?.Trim()).Length);

// A ?. on a value-typed member gives a nullable value, so a comparison is
// false rather than an error. ?? makes the intent explicit.
Console.WriteLine(text?.Length > 0);
Console.WriteLine((text?.Length ?? 0) > 0);

// Output:
// (empty)
// False
// False

Null-conditional assignment, new in C# 14

Until C# 14, ?. worked only when reading. Writing through a reference that might be null meant an if statement, which is why older code is full of them. The important detail is that the right-hand side is evaluated only when the left side is non-null, so a method call there is skipped entirely rather than called and discarded.

Example · csharp
using System.Text;

StringBuilder? buffer = new StringBuilder("hello world");
StringBuilder? absent = null;

// C# 14: ?. and ?[] may appear on the left of an assignment.
// Setting Length on a StringBuilder trims it, leaving "hello".
buffer?.Length = 5;

// The identical line against a null variable does nothing and throws nothing.
absent?.Length = 5;

// Before C# 14, that first line had to be written out.
if (buffer != null)
{
    buffer.Length = 4;    // now "hell"
}

// Compound assignment is allowed too. Increment and decrement are not:
// buffer?.Length++ does not compile.
buffer?.Length -= 1;      // now "hel"

Console.WriteLine(buffer);
Console.WriteLine(absent == null);

// Output:
// hel
// True

Discussion

  • Be the first to comment on this lesson.