Nullable Reference Types
Concept
Nullable Value Types Value types cannot normally be null. int? (short for Nullable<int>) adds a "no value" state:
HasValue, Value (throws if empty), GetValueOrDefault(fallback).
Arithmetic with null yields null ("lifted" operators); comparisons with null yield false.
Nullable Reference Types (NRT) Since C# 8, with <Nullable>enable</Nullable> (the default in new projects), the compiler tracks nullability of reference types:
Should never be null May be null — must be checked before dereferencing
Null-forgiving operator: "I know this is not null" (silences the warning, adds no check)
Null-state analysis follows if (x is not null), ??, ?., pattern matching and attributes such as [NotNullWhen(true)] to know when a variable is safe. Violations are warnings (CS8600, CS8602, CS8618, …), not errors — turn them into errors in CI with <WarningsAsErrors>nullable</WarningsAsErrors>.
Pattern: if (x is null) / if (x is not null) (cannot be fooled by operator overloads).
ArgumentNullException.ThrowIfNull(x) at API boundaries.
??, ??= and ?. (Module 7).
API Design With Nullability Annotate return types honestly (string? FindModel(...)), prefer non-null parameters, and use TryXxx(out T value) with [NotNullWhen(true)] when a lookup can fail.
Why It Matters
NullReferenceException is the most common .NET runtime error. Nullable reference types move most of these bugs to compile time.
Syntax
int? n = null; Nullable<int> m = 5;
string? maybe = Find(); string sure = maybe ?? "default";
if (maybe is not null) Use(maybe);
string forced = maybe!; // only when you are certainExample
#nullable enable
// Nullable value types
int? rpm = null;
Console.WriteLine($"rpm.HasValue = {rpm.HasValue}, rpm ?? 0 = {rpm ?? 0}, GetValueOrDefault(800) = {rpm.GetValueOrDefault(800)}");
rpm = 3000;
Nullable<int> same = rpm;
Console.WriteLine($"rpm.Value = {rpm.Value}, same == rpm: {same == rpm}");
int? missing = null;
Console.WriteLine($"null + 5 = {(missing + 5)?.ToString() ?? "null"}, null > 3 = {missing > 3}");
// Nullable reference types: annotations + flow analysis
string? model = FindModel("VIN-2");
Console.WriteLine($"length: {model?.Length ?? -1}");
if (model is not null)
Console.WriteLine($"model: {model.ToUpper()}"); // flow analysis knows model is not null here
string definite = FindModel("VIN-1") ?? "unknown"; // null-coalescing
Console.WriteLine($"definite: {definite}");
// Null-forgiving operator: "trust me" - only when you know better than the compiler
string forced = FindModel("VIN-1")!;
Console.WriteLine($"forced: {forced}");
// Null checks
var ecu = new Ecu("VCU", null);
Console.WriteLine($"firmware: {ecu.Firmware ?? "(none)"}, describe: {ecu.Describe()}");
try
{
_ = new Ecu(null!, "1.0");
}
catch (ArgumentNullException ex)
{
…Expected Result
rpm.HasValue = False, rpm ?? 0 = 0, GetValueOrDefault(800) = 800
rpm.Value = 3000, same == rpm: True
null + 5 = null, null > 3 = False
length: -1
definite: Aurora EV
forced: Aurora EV
firmware: (none), describe: VCU (unflashed)
ArgumentNullException for 'code'Common Mistakes
The compiler warns about possible null dereferences — real warnings:
Sprinkling ! to silence warnings instead of fixing the logic.
Ignoring nullable warnings (they are real bugs waiting to happen).
Using .Value on an empty int? (throws InvalidOperationException).
Forgetting that string.Empty and null are different.
#nullable enable
string? name = GetName();
Console.WriteLine(name.Length); // warning: possible null dereference
string notNull = null; // warning: null to non-nullable
Console.WriteLine(notNull);
static string? GetName() => null;m21_warn.cs(3,19): warning CS8602: Dereference of a possibly null reference.
m21_warn.cs(4,18): warning CS8600: Converting null literal or possible null value to non-nullable type.Best Practices
Enable nullable reference types in every project and treat warnings as errors in CI.
Make the non-null path the default; use ? only where "no value" is meaningful.
Validate external input (JSON, databases, user input) at the boundary.
Prefer is null / is not null over == null.
Interview Relevance
High: "what are nullable reference types?", "int? vs string?", "what does ! do?", "how do you avoid NullReferenceException?".
Practice Questions
Enable NRT in an old code sample and fix all warnings.
Write bool TryGetEcu(string code, [NotNullWhen(true)] out Ecu? ecu).
Explain the difference between int? (a struct) and string? (an annotation).
Compute the average of a list of double? readings, ignoring nulls.
Quick Quiz
Q1. (Predict the output) int? m = null; Console.WriteLine(m > 3); Answer: False (verified).
Q2. (True/False) The null-forgiving operator ! adds a runtime null check.
Answer: False. It only suppresses the compiler warning.
Q3. (Multiple choice) Which warning code means "dereference of a possibly null reference"? A) CS8600 B) CS8602 C) CS0029 D) CS0103 Answer: B (verified).
Q4. (Predict the output) int? missing = null; Console.WriteLine(missing + 5 is null);
Answer: True — lifted arithmetic with null yields null.
Coding Challenge
Write double? AverageOrNull(IEnumerable<double?> readings) that ignores nulls and returns null when there are no values.
static double? AverageOrNull(IEnumerable<double?> readings)
{
var values = readings.Where(r => r.HasValue).Select(r => r!.Value).ToList();
return values.Count == 0 ? null : values.Average();
}
Console.WriteLine(AverageOrNull([12.1, null, 12.5]));
Console.WriteLine(AverageOrNull([null, null]) is null);