📝 Lesson 3.3: Nullable Reference Types
The NullReferenceException is one of the most common runtime crashes in all of programming. Nullable reference types let the C# compiler warn you about potential null problems before your program ever runs.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain the null problem and why it causes crashes
- Distinguish nullable (
string?) from non-nullable (string) references - Respond to the compiler's nullability warnings
- Use
?.,??, and??=to handle null safely and concisely - Use the null-forgiving
!operator responsibly
Estimated Time: 60 minutes
Project: Make code null-safe and satisfy the compiler's null analysis.
In This Lesson
The Null Problem
null means "no value — this reference points to nothing." Trying to use a null reference (call a method, read a property) throws a NullReferenceException and crashes your program. You met this exception in Lesson 1.1.
string name = null;
Console.WriteLine(name.Length); // 💥 NullReferenceException at runtime
📖 The "billion-dollar mistake"
The inventor of the null reference, Tony Hoare, later called it his "billion-dollar mistake" for all the crashes it has caused. C#'s nullable reference types feature is designed to catch these problems at compile time instead of runtime.
The core idea: let you declare your intent — "this can be null" or "this must never be null" — and have the compiler check that you honor it.
Nullable Value Types (Recap)
Value types like int and bool normally can't be null. But sometimes you need "an int, or no value at all" — say, an optional age. Add ? to make a nullable value type:
int? maybeAge = null; // an int that can also be null
maybeAge = 30;
if (maybeAge.HasValue)
{
Console.WriteLine(maybeAge.Value); // 30
}
int definiteAge = maybeAge ?? 0; // use 0 if null (?? explained below)
This is the generic Nullable<T> from Lesson 1.2, with ? as convenient shorthand. Nullable reference types use the same ? symbol — but they work a little differently, as we'll see.
Nullable Reference Types
Reference types (like string and your own classes) have always been able to hold null. The nullable reference types feature doesn't change that at runtime — instead, it adds compile-time analysis. In a nullable-aware project, you declare which references are allowed to be null:
| Declaration | Meaning |
|---|---|
string name | Non-nullable: should never be null. The compiler warns if it might be. |
string? name | Nullable: may be null. The compiler makes you check before using it. |
string name = "Ada"; // non-nullable
string? nickname = null; // nullable — this is fine
Console.WriteLine(name.Length); // OK — compiler knows it's not null
Console.WriteLine(nickname.Length); // ⚠️ Warning — nickname might be null!
The compiler traces your code. Once you've checked for null, the warning goes away inside that block — it understands the guard:
if (nickname != null)
{
Console.WriteLine(nickname.Length); // OK here — you proved it's not null
}
💡 Enabling nullable analysis
Modern project templates enable this by default via <Nullable>enable</Nullable> in the .csproj (or #nullable enable at the top of a file). When it's on, the ? annotations become meaningful and the compiler starts guiding you. These are warnings, not errors — but treat them seriously; each one is a potential crash.
never null"] B -->|"string? (with ?)"| D["Compiler requires
a null check before use"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style C fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style D fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
Null-Safe Operators
C# has three concise operators that make working with possibly-null values painless.
Null-conditional ?.
The ?. operator (you saw it raising events in Lesson 2.2) accesses a member only if the left side isn't null; otherwise the whole expression is null — no crash:
string? name = null;
int? length = name?.Length; // null (not a crash) because name is null
Console.WriteLine(length); // (prints nothing / empty)
name = "Ada";
Console.WriteLine(name?.Length); // 3
Null-coalescing ??
The ?? operator supplies a fallback value when the left side is null:
string? input = null;
string display = input ?? "(no name)"; // use the fallback if input is null
Console.WriteLine(display); // (no name)
// Combine ?. and ?? for a safe default:
int length = name?.Length ?? 0; // 0 if name is null
Null-coalescing assignment ??=
The ??= operator assigns a value only if the variable is currently null — handy for lazy defaults:
string? config = null;
config ??= "default-settings"; // assigns because config was null
config ??= "other"; // does nothing — config is no longer null
Console.WriteLine(config); // default-settings
✅ The everyday combo
value?.Something ?? fallback is one of the most useful idioms in C#: "get this member if the value exists, otherwise use a fallback." It replaces several lines of null-checking with one clear expression.
The Null-Forgiving Operator
Occasionally you know a value isn't null even though the compiler can't prove it. The null-forgiving operator ! tells the compiler "trust me, this isn't null here," suppressing the warning:
string? GetName() => "Ada"; // always returns a value in practice
string name = GetName()!; // '!' — I promise this isn't null
Console.WriteLine(name.Length);
⚠️ Use ! sparingly — it's a promise, not a fix
The ! operator only silences the warning; it does nothing at runtime. If you're wrong and the value is null, you're back to a NullReferenceException. Prefer a real null check whenever you can; reach for ! only when you're genuinely certain and the compiler simply can't see it.
Practical Patterns
A few habits make nullable-aware code pleasant to write:
✅ Guard early with pattern matching
Combine null checks with the pattern matching from Lesson 3.2:
void Greet(string? name)
{
if (name is null)
{
Console.WriteLine("Hello, stranger!");
return; // early exit — 'name' is non-null after this
}
Console.WriteLine($"Hello, {name}!"); // no warning — guarded above
}
💡 Initialize non-nullable properties
If a class has a non-nullable string property, the compiler wants to be sure it's never left null. Set it in a constructor, give it a default, or mark it required so callers must provide it:
class User
{
public required string Name { get; set; } // callers must set it
public string? Bio { get; set; } // optional — may be null
}
💡 The mindset shift: Treat every nullable warning as the compiler pointing at a possible future crash. Either prove the value isn't null (a check), provide a fallback (??), or reconsider whether the reference should be nullable at all. This habit eliminates a whole class of bugs.
Exercise & Quiz
🏋️ Exercise: A Null-Safe Profile Printer
Objective: Handle nullable data with ?., ??, and guards.
Instructions:
- Create a new project called
NullSafety. - Define
class Profile { public string Name; public string? Bio; public string? Website; }(setNamevia a constructor). - Write
void Print(Profile? profile)that: ifprofileis null, prints "No profile."; otherwise prints the name, the bio or "(no bio)" if null, and the website's length or 0 using?.and??. - Call it with a full profile, a profile missing bio/website, and
null.
Starter Code:
Print(new Profile("Ada") { Bio = "Mathematician", Website = "ada.dev" });
Print(new Profile("Grace"));
Print(null);
void Print(Profile? profile)
{
// TODO: guard against null profile
// TODO: print Name, Bio ?? "(no bio)", and Website?.Length ?? 0
}
class Profile
{
public string Name;
public string? Bio;
public string? Website;
public Profile(string name) { Name = name; }
}
💡 Hint
Start with if (profile is null) { Console.WriteLine("No profile."); return; }. Then profile.Bio ?? "(no bio)" for the bio, and profile.Website?.Length ?? 0 for the website length.
✅ Solution
Print(new Profile("Ada") { Bio = "Mathematician", Website = "ada.dev" });
Print(new Profile("Grace"));
Print(null);
void Print(Profile? profile)
{
if (profile is null)
{
Console.WriteLine("No profile.");
return;
}
Console.WriteLine($"Name: {profile.Name}");
Console.WriteLine($"Bio: {profile.Bio ?? "(no bio)"}");
Console.WriteLine($"Website length: {profile.Website?.Length ?? 0}");
Console.WriteLine("---");
}
class Profile
{
public string Name;
public string? Bio;
public string? Website;
public Profile(string name) { Name = name; }
}
Output:
Name: Ada
Bio: Mathematician
Website length: 7
---
Name: Grace
Bio: (no bio)
Website length: 0
---
No profile.
🎯 Quick Quiz
Question 1: What's the difference between string and string? in a nullable-aware project?
Question 2: What does name?.Length evaluate to when name is null?
Question 3: What does the null-forgiving operator ! (as in value!) actually do?
Summary
🎉 Key Takeaways
- Using a
nullreference throws aNullReferenceException; nullable reference types help catch this at compile time. stringmeans "shouldn't be null";string?means "may be null" — and the compiler enforces a check before use.?.safely accesses members (null if the left is null);??supplies a fallback;??=assigns only when null.- The idiom
value?.Member ?? fallbackhandles null in one clean line. - The null-forgiving
!only silences the warning — use it rarely; prefer real checks, guards,requiredmembers, or defaults.
📚 Additional Resources
- Nullable reference types — Microsoft Docs
- ?? and ??= operators — reference
- Null-conditional operators ?. and ?[] — reference
🚀 What's Next?
That completes Module 3 — your C# is now modern, concise, and null-safe! In Module 4, your programs start interacting with the outside world. First up: Lesson 4.1: Working with Files and Streams, where data finally outlives the program run.
🎉 Module 3 complete!
Records, patterns, and null-safety make your code clean and robust. Next: reading and writing real data.