The most common crash in C# programs has always been the NullReferenceException: code tries to use an object, but the variable holds null. The inventor of null references famously called them his "billion-dollar mistake". Modern C# fights back with nullable reference types: the compiler tracks which variables might be null and warns you before the program runs whenever you use one without checking.
With this feature turned on, which it is by default in every new .NET project, string means "never null" and string? means "might be null". C# also has compact operators for handling nulls: ?. to call a member only when the object exists, ?? to supply a default, and ??= to assign only when a variable is null.
The second half of this lesson is pattern matching, a family of features that let is and switch test the shape of data: its type, its property values, ranges, and even the contents of a list. Patterns turn long chains of if statements into short, readable rules, and they work hand in hand with null checks.
Nullable reference types
string name = "Ada"; // must never be null
string? nickname = null; // allowed to be null
Console.WriteLine(name.Length);
Console.WriteLine(nickname.Length); // warning CS8602: possible null dereference- Nullable checking is switched on with
<Nullable>enable</Nullable>in the project file, the default in new projects. - The warnings are about intent: they don't change how the program runs, but each one points at a possible crash.
- After a check such as
if (nickname is not null), the compiler knows the variable is safe inside the block. - The null-forgiving operator
!(nickname!.Length) tells the compiler "trust me, it's not null". Use it rarely; if you're wrong, the program still crashes. - Value types use the same
?:int? age = null;withage.HasValueandage.GetValueOrDefault().
Null operators
| Operator | Example | Meaning |
|---|---|---|
?. | user?.Email | Null if user is null, otherwise its Email |
?[] | orders?[0] | Null-safe indexing |
?? | name ?? "Guest" | Use the right side when the left is null |
??= | cache ??= Load() | Assign only if currently null |
is null / is not null | if (order is null) return; | Clear, reliable null checks |
They chain: customer?.Address?.City ?? "Unknown" reads a nested value safely in one line.
Pattern matching
Patterns test values in is expressions and switch expressions:
object item = 42;
if (item is int n && n > 10) { } // type pattern
if (order is { Status: "Paid", Total: > 100 }) { } // property pattern
string size = weight switch // relational patterns
{
< 1 => "Small",
>= 1 and < 10 => "Medium",
_ => "Large"
};
if (point is (0, 0)) { } // positional pattern
if (args is [var first, ..]) { } // list pattern- Type patterns check a type and give a typed variable.
- Property patterns
{ Prop: pattern }check properties, and can nest. - Relational (
<,>=) and logical (and,or,not) patterns combine conditions. - Positional patterns use deconstruction, which records support automatically.
- List patterns match arrays and lists by their elements;
..matches any number of items.
Telling the compiler a value isn't null
The compiler follows your null checks within a method, but it can't see inside other methods unless they say what they guarantee. Many .NET methods already do. After if (!string.IsNullOrEmpty(s)), the compiler knows s isn't null, and after ArgumentNullException.ThrowIfNull(customer); it treats customer as non-null for the rest of the method.
Your own "try" methods can make the same promise with attributes from System.Diagnostics.CodeAnalysis:
using System.Diagnostics.CodeAnalysis;
static bool TryFindUser(Dictionary<int, string> users, int id,
[NotNullWhen(true)] out string? name)
=> users.TryGetValue(id, out name);
if (TryFindUser(users, 1, out string? name))
Console.WriteLine(name.Length); // no warning: true means name is set[NotNullWhen(true)] says "when this method returns true, the out value isn't null". Without it, the compiler warns about name.Length even inside the if, and people are tempted to silence it with !. Related attributes include [MaybeNullWhen(false)] and [NotNull]. They describe your code to the compiler; they don't add any checks at run time.
C# Advanced Syntax
Nullable and pattern syntax
string? s = null;
int len = s?.Length ?? 0;
s ??= "default";
if (s is not null) { }
int? maybe = null; int value = maybe ?? -1;
if (shape is Circle { Radius: > 10 } c) { }
string r = n switch { < 0 => "neg", 0 => "zero", _ => "pos" };
if (p is (> 0, > 0)) { }
if (list is [1, 2, ..]) { }
if (list is [.., var last]) { }C# Advanced Summary
- With nullable reference types, string can't be null and string? can; the compiler warns about unsafe use.
- ?. ?[] ?? and ??= handle nulls concisely; is null and is not null check for null.
- The null-forgiving ! silences a warning but doesn't prevent crashes.
- Type, property, relational and logical patterns test values in is and switch.
- Positional and list patterns match deconstructed values and collection contents.
C# Advanced Examples
Each example shows the code, the output it produces and what each part does.
Handling missing data safely
var customers = new List<Customer>
{
new("Ada", new Address("Leeds", "LS1 4AP")),
new("Ben", new Address(null, "M1 1AE")),
new("Cy", null)
};
foreach (Customer c in customers)
{
string city = c.Address?.City ?? "Unknown city";
int postcodeLength = c.Address?.Postcode?.Length ?? 0;
Console.WriteLine($"{c.Name}: {city} ({postcodeLength})");
}
string? cachedReport = null;
cachedReport ??= BuildReport();
cachedReport ??= BuildReport();
Console.WriteLine(cachedReport);
string BuildReport()
{
Console.WriteLine("Building report...");
return "Report ready";
}
record Address(string? City, string? Postcode);
record Customer(string Name, Address? Address);Ada: Leeds (7)
Ben: Unknown city (6)
Cy: Unknown city (0)
Building report...
Report readyLine-by-line explanation
- Line 10?. stops at the first null, and ?? supplies a fallback.
- Line 11Chaining works through several possibly-null levels.
- Lines 16 to 17??= only assigns when the variable is null, so the report is built once.
- Lines 26 to 27The ? marks show exactly which values may be missing.
Shipping rules with property patterns
var orders = new[]
{
new Order("UK", 120m, false),
new Order("UK", 30m, false),
new Order("FR", 80m, false),
new Order("US", 45m, true),
new Order("JP", 200m, false)
};
foreach (var o in orders)
{
Console.WriteLine($"{o.Country} {o.Total,6}: {Shipping(o)}");
}
string Shipping(Order order) => order switch
{
{ IsMember: true } => "Free for members",
{ Country: "UK", Total: >= 50 } => "Free UK delivery",
{ Country: "UK" } => "3.99",
{ Country: "FR" or "DE" or "ES", Total: > 60 } => "5.99",
{ Country: "FR" or "DE" or "ES" } => "9.99",
_ => "Quote at checkout"
};
record Order(string Country, decimal Total, bool IsMember);UK 120: Free UK delivery
UK 30: 3.99
FR 80: 5.99
US 45: Free for members
JP 200: Quote at checkoutLine-by-line explanation
- Lines 17 to 19Property patterns check one or more properties at once.
- Line 17Arms are tried from top to bottom, so members are handled first.
- Line 18Total: >= 50 combines a property pattern with a relational pattern.
- Line 20or matches any of several values for one property.
- Line 22The discard handles every remaining case.
Type, positional and list patterns
object[] values = { 42, "hello", 3.5, null!, new Point(0, 0), new Point(3, -1), new[] { 1, 2, 3 } };
foreach (object v in values)
{
string description = v switch
{
null => "null",
int n when n > 10 => $"big int {n}",
int n => $"int {n}",
string { Length: > 3 } s => $"long string '{s}'",
double d => $"double {d}",
Point(0, 0) => "the origin",
Point(var x, < 0) => $"below the x axis at x={x}",
int[] [1, .., var last] => $"array starting with 1, ending with {last}",
_ => "something else"
};
Console.WriteLine(description);
}
record Point(int X, int Y);big int 42
long string 'hello'
double 3.5
null
the origin
below the x axis at x=3
array starting with 1, ending with 3Line-by-line explanation
- Line 7The null pattern handles null before anything else.
- Line 8A type pattern with a when clause adds an extra condition.
- Line 10A type pattern and a property pattern combined.
- Line 12A positional pattern deconstructs the record and compares both values.
- Line 13var x captures a value while < 0 tests the other.
- Line 14A list pattern checks the first element and captures the last; .. skips the middle.
Common C# Advanced Mistakes
Silencing warnings with !
string city = customer.Address!.City!; // the warning goes away, the crash doesn'tstring city = customer.Address?.City ?? "Unknown";Comparing with == null on types that overload ==
if (order == null) { } // can call a custom == operatorif (order is null) { } // always a true null checkBroad patterns before narrow ones
string label = n switch
{
> 0 => "positive",
> 100 => "large", // error CS8510: already handled by a previous arm
_ => "other"
};string label = n switch
{
> 100 => "large",
> 0 => "positive",
_ => "other"
};C# Advanced in Real Projects
Null safety and patterns in .NET code
Since .NET 6, every new project has nullable reference types turned on, and the .NET libraries themselves are annotated, so the compiler knows that Dictionary.GetValueOrDefault can return null. ASP.NET Core model binding and Entity Framework use these annotations to decide which fields are required. Pattern-matching switch expressions are common in business rules, such as pricing, shipping and permissions, and in code that handles different message or event types.
C# Advanced FAQ
What does the question mark after a type mean in C#?
On a value type, int? creates a real nullable type, Nullable<int>, which can hold a number or null and has HasValue and Value. On a reference type with nullable checking enabled, string? is an annotation telling the compiler the variable may be null, so it warns you before you use it unsafely. In both cases it signals "this might be missing".
What is the difference between ?? and ?. in C#?
?. is the null-conditional operator: user?.Email gives null instead of throwing when user is null. ?? is the null-coalescing operator: name ?? "Guest" supplies a fallback when the left side is null. They're often used together, as in user?.Email ?? "no email".
Should I use is null or == null in C#?
Prefer is null and is not null. They always check for a real null reference, while == can be redefined by a type's own operator overload and give surprising results. Both compile to the same check for ordinary types, so is null costs nothing extra.
How do I fix warning CS8602 possible null dereference?
Check the value before using it, for example with if (x is null) return; or x?.Length ?? 0, provide a default with ??, or change the declaration to a non-nullable type if null should never happen. Use the ! operator only when you're certain the value can't be null and the compiler simply can't see why.
What is a list pattern in C#?
A list pattern, available since C# 11, matches an array or list by its elements, written in square brackets. nums is [1, 2, 3] checks the exact contents, args is [var first, ..] matches one or more items and captures the first, and .. stands for any number of items. They work in is expressions and switch expressions.
C# Advanced Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the declaration so the variable may hold null.
string middleName = null;
What does this print?
string? s = null;
Console.WriteLine(s?.Length ?? -1);
What does x ??= 10; do?
What does this print?
var p = new Pt(2, 5);
Console.WriteLine(p is { X: > 0, Y: > 4 });
record Pt(int X, int Y);
Does the null-forgiving operator ! prevent a NullReferenceException?
C# Advanced Quiz
5 questions to check what you learned.
What does
string?mean with nullable reference types enabled?- An optional parameter
- A string that can't be null
- A nullable int
- A string that may be null
What does
user?.Emailreturn when user is null?- null
- An empty string
- It throws
- "null"
Which pattern checks property values?
- Type pattern
- Property pattern { Name: value }
- List pattern
- Discard pattern
What does
..mean in a list pattern like [1, .., 9]?- A range of numbers
- Exactly two elements
- Any number of elements in between
- An error
Why is
is nullpreferred over== null?- It can't be changed by a custom == operator, so it's always a true null check
- It's faster to type
- == null doesn't compile
- It works only for strings