C# · Course navigation

C# nullable values, patterns, and switch expressions

Handle missing values safely and branch on the type and value of an object.

On this page
Before you start

C# types and conditionals.

  • Handle missing values safely and branch on the type and value of an object.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

A value type such as int cannot normally be null. int? is a nullable int that either holds a value or is null. HasValue tests which, and the null-coalescing operator ?? supplies a default. The null-conditional operator ?. stops and gives null if the object is null, so text?.Length is null for a null string instead of throwing.

Pattern matching tests the shape of a value. A switch expression returns a value from the first matching arm: type patterns like int n, constant patterns like null, and a when clause for extra conditions. The _ arm matches anything else. Arms are checked in order, so more specific arms must come first. Tuples let a method return several values, which the caller can unpack with deconstruction.

Code example

C#
using System;
using System.Globalization;

class Program {
  static (int Min, int Max) MinMax(int[] values) {
    int min = values[0], max = values[0];
    foreach (int v in values) {
      if (v < min) min = v;
      if (v > max) max = v;
    }
    return (min, max);
  }

  static void Main() {
    int? maybe = null;
    Console.WriteLine(maybe.HasValue + " " + (maybe ?? -1));
    string text = null;
    Console.WriteLine(text?.Length ?? 0);

    object[] things = { 42, 7, "hello", 3.5, null, new[] { 1, 2, 3 } };
    foreach (object thing in things) {
      string description = thing switch {
        int n when n > 40 => "big int " + n,
        int n => "int " + n,
        string s => "string of length " + s.Length,
        double x => "double " + x.ToString(CultureInfo.InvariantCulture),
        null => "null",
        int[] array => "array with " + array.Length + " items",
        _ => "something else",
      };
      Console.WriteLine(description);
    }

    var (min, max) = MinMax(new[] { 4, 9, 2, 7 });
    Console.WriteLine(min + " " + max);
  }
}
Output
False -1
0
big int 42
int 7
string of length 5
double 3.5
null
array with 3 items
2 9

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1using System;
2using System.Globalization;
3
4class Program {
5 static (int Min, int Max) MinMax(int[] values) {
6 int min = values[0], max = values[0];
7 foreach (int v in values) {
8 if (v < min) min = v;
9 if (v > max) max = v;
10 }
11 return (min, max);
12 }
13
14 static void Main() {
15 int? maybe = null;
16 Console.WriteLine(maybe.HasValue + " " + (maybe ?? -1));
17 string text = null;
18 Console.WriteLine(text?.Length ?? 0);
19
20 object[] things = { 42, 7, "hello", 3.5, null, new[] { 1, 2, 3 } };
21 foreach (object thing in things) {
22 string description = thing switch {
23 int n when n > 40 => "big int " + n,
24 int n => "int " + n,
25 string s => "string of length " + s.Length,
26 double x => "double " + x.ToString(CultureInfo.InvariantCulture),
27 null => "null",
28 int[] array => "array with " + array.Length + " items",
29 _ => "something else",
30 };
31 Console.WriteLine(description);
32 }
33
34 var (min, max) = MinMax(new[] { 4, 9, 2, 7 });
35 Console.WriteLine(min + " " + max);
36 }
37}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
A nullable int holding no value.—
—
HasValue is False, and ?? supplies -1.—
False -1
text is null, so text?.Length is null and ?? turns it into 0.—
False -1
0
The first arm only matches ints above 40. Order matters.—
False -1
0
The discard arm catches anything not matched earlier.—
False -1
0
42 matches the first arm, 7 the second, then string, double, null, and the array.—
False -1
0
big int 42
int 7
string of length 5
double 3.5
null
array with 3 items
Deconstruct the returned tuple into two variables.—
False -1
0
big int 42
int 7
string of length 5
double 3.5
null
array with 3 items
The smallest and largest values.—
False -1
0
big int 42
int 7
string of length 5
double 3.5
null
array with 3 items
2 9

Behind the scenes

How C# processes this code

C# compiles to code for the .NET runtime, which invokes the program entry point.

Cost and common mistakes

Each operation is O(1); MinMax is O(n).

  • A switch expression with no matching arm throws a SwitchExpressionException at run time. Add _ unless the compiler proves the cases are complete.
  • A nullable reference type annotation (string?) is a compile-time warning system. It does not change run-time behaviour.
  • Reading maybe.Value when it has no value throws InvalidOperationException. Check HasValue or use ??.

Try it yourself

What does maybe.GetValueOrDefault() return when maybe is null?

Reveal answer and reasoning

0, the default value of int. GetValueOrDefault(5) would return 5.

Further reading