C# nullable values, patterns, and switch expressions
Handle missing values safely and branch on the type and value of an object.
On this page
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
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);
}
}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
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); }}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
| Step / line | What happens | State after step | Output 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.