Skip to content

.NET Core · Fundamentals

Null Safety in C#: Nullable Types and Nullable Reference Types

9 min read Updated 2026-06-22 Share:

Practice Nullable Types interview questions

Why null handling comes up in every C# interview

Tony Hoare called null "my billion-dollar mistake." In C#, null has evolved from a simple runtime hazard (NullReferenceException) into a first-class part of the type system with C# 8's nullable reference types. Interviewers probe null handling because it directly relates to code correctness, API clarity, and defensive programming. They want to see you distinguish between "this value is optional" and "this should never be null," use the right operator for each case, and understand what C# 8's nullable annotations actually do at runtime.

Two completely different kinds of null in C#

C# has two orthogonal null systems that are easy to confuse:

Nullable value types (int?)Nullable reference types (string?)
IntroducedC# 2.0C# 8.0
Runtime effectReal — Nullable<T> struct existsNone — compile-time annotation only
Runtime typeSystem.Nullable<int>System.String (unchanged)
Opt-in requiredNo — always availableYes — <Nullable>enable</Nullable>

Understanding this distinction is crucial. int? is a genuine runtime type that changes how the CLR represents the value. string? is purely a compile-time hint — at runtime it is indistinguishable from string.

Nullable value types — Nullable<T>

All C# value types (int, double, bool, DateTime, struct) are non-nullable by default — they cannot be set to null. Nullable<T> (written as T?) wraps a value type and adds a HasValue flag.

// int — can only hold numeric values
int a = 42;
// a = null; // CS0037 — int cannot be null

// int? — can hold null OR numeric values
int? b = 42;
int? c = null;

// The two properties:
Console.WriteLine(b.HasValue);  // True
Console.WriteLine(b.Value);     // 42

Console.WriteLine(c.HasValue);  // False
// Console.WriteLine(c.Value);  // InvalidOperationException!
Console.WriteLine(c.GetValueOrDefault()); // 0 — safe, no throw
Console.WriteLine(c.GetValueOrDefault(-1)); // -1 — custom fallback

At runtime, Nullable<int> is a struct containing bool HasValue and int Value. The compiler synthesises special handling: int? x = null compiles to Nullable<int> x = new Nullable<int>() (where HasValue = false).

Where nullable value types are essential

Database columns that allow NULL are the primary use case:

// Entity Framework model:
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public DateTime? DiscontinuedDate { get; set; } // null = still active
    public decimal? Weight { get; set; }             // null = not measured
}

// Querying:
var active = db.Products.Where(p => p.DiscontinuedDate == null).ToList();

Nullable value types also appear in: optional form fields, configuration values with defaults, three-state booleans (bool? for true/false/unknown), and nullable method return values when "no result" is a valid state distinct from any value.

The null-coalescing operator ??

?? returns the left operand if non-null, otherwise the right operand. It short-circuits — the right side is not evaluated if the left is non-null.

string? name = GetName(); // may return null

// Verbose form:
string display = name != null ? name : "Guest";

// With ??:
string display = name ?? "Guest";

// Chain for fallback hierarchy:
string value = GetFromCache()
            ?? GetFromDatabase()
            ?? GetFromConfig()
            ?? "default";

// Nullable value type:
int? optionalCount = GetCount(); // may be null
int  count         = optionalCount ?? 0;

// Lazy field initialisation:
private string? _label;
public string Label => _label ?? (_label = ComputeLabel()); // initialise on first access

Null-coalescing assignment ??=

??= assigns the right side to the variable only if the variable is currently null.

// Without ??=:
if (_cache == null)
    _cache = new Dictionary<string, object>();

// With ??= (C# 8+):
_cache ??= new Dictionary<string, object>();

// Property initialisation:
private List<string>? _items;
public List<string> Items => _items ??= new List<string>();
// First access creates the list; subsequent accesses return it

The null-conditional operator ?.

?. navigates a chain of member accesses, returning null at the first null link instead of throwing NullReferenceException. It converts the result to a nullable type when the final member is a value type.

User? user = GetUser(id); // may return null

// Without ?.: three levels of null checking
string? city = null;
if (user != null && user.Address != null)
    city = user.Address.City;

// With ?.:
string? city = user?.Address?.City; // null if user or Address is null

// With value types — result becomes nullable:
int? orderCount = user?.Orders?.Count; // int? because Count is int

// Safe event invocation — the classic use case:
EventHandler? OnChanged;
OnChanged?.Invoke(this, EventArgs.Empty); // skip if no subscribers

// Index notation:
string? firstOrderItem = user?.Orders?[0]?.Description;

// Combine with ??: provide a default
string display = user?.Name ?? "Guest";
int    count   = user?.Orders?.Count ?? 0;

The ?. chain short-circuits: once a null is encountered, the entire expression evaluates to null without executing further member accesses or method calls.

Nullable reference types — C# 8

Before C# 8, string could be null at any time — the type system provided no way to distinguish "this string is always set" from "this string might be absent." C# 8 introduced a compile-time annotation layer (not a runtime change) that lets you express this intent.

#nullable enable

// Non-nullable — must always be a valid string:
string name = "Alice";     // fine
// name = null;             // CS8600: cannot assign null to non-nullable string

// Nullable — can be null:
string? nickname = null;   // fine
Console.WriteLine(nickname.Length); // CS8602: dereference of possibly null reference

// Safe access patterns the compiler accepts:
if (nickname != null)
    Console.WriteLine(nickname.Length); // flow analysis narrows to non-null

Console.WriteLine(nickname?.Length);    // null-conditional returns null safely
Console.WriteLine(nickname?.Length ?? 0); // with default

Enabling nullable reference types

<!-- Project-wide in .csproj — recommended for all new projects -->
<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>
// Per-file (incremental migration of existing code):
#nullable enable    // enable for remainder of this file
#nullable disable   // disable (for legacy code you haven't migrated yet)
#nullable restore   // restore to project default

Annotations only (#nullable enable annotations) enables the annotation syntax without emitting warnings — useful for marking up APIs before you are ready to fix all call sites.

Flow analysis

The C# compiler performs null-state flow analysis through branches, null checks, and pattern matching:

#nullable enable

void Process(string? value)
{
    // value is "maybe null" here
    Console.WriteLine(value.Length); // warning

    if (value == null)
        return;

    // value is "not null" here — flow analysis narrows it:
    Console.WriteLine(value.Length); // no warning

    // Pattern matching also narrows:
    if (value is { Length: > 0 })
        Console.WriteLine(value.ToUpper()); // non-null
}

The null-forgiving operator !

! suppresses a nullable warning by asserting to the compiler "I know this is not null." It has zero runtime effect — if you are wrong, you get a NullReferenceException with no warning.

#nullable enable

// Legitimate use: field set in a method the analyser cannot track
private string _name = null!; // set in Initialize(), before any use

// Test setup:
private MyService _service = null!; // assigned in [SetUp] method

// After a manually verified check:
Debug.Assert(result != null);
Console.WriteLine(result!.Name); // analyser can't track Assert; ! suppresses warning

// Bad use — just hiding the problem:
string? couldBeNull = GetData();
Console.WriteLine(couldBeNull!.Length); // might blow up at runtime

Use ! sparingly — treat each occurrence as a TODO comment that deserves justification. If you find yourself writing ! frequently, refactor the code so the analyser can infer non-nullability.

Pattern matching and null

Pattern matching in C# provides clean, composable null handling that integrates with the compiler's flow analysis:

#nullable enable

object? Classify(object? value) => value switch
{
    null               => "nothing",                    // null arm
    int n when n < 0   => $"negative: {n}",
    int n              => $"positive: {n}",
    string { Length: 0 } => "empty string",            // property pattern
    string s           => $"string: {s}",
    _                  => "something else",
};

// 'is' pattern with type check and null check in one:
if (value is string s)  // only matches if value is non-null AND is string
    Console.WriteLine(s.ToUpper());

// 'is not null' — preferred over '!= null' since C# 9:
if (value is not null)
    Console.WriteLine(value.GetType());

// Property pattern with null check:
if (user is { Address.City: "London" })  // safely checks City even if Address could be null
    Console.WriteLine("London user");

Boxing and null — the special case

When a null Nullable<T> is boxed, the result is a true null reference (not a boxed wrapper around null). When a non-null Nullable<T> is boxed, the underlying value T is boxed — not the Nullable<T> wrapper.

int?   a = 42;
object boxedA = a;         // boxes to boxed int (System.Int32) — NOT Nullable<int>
Console.WriteLine(boxedA.GetType().Name); // "Int32"

int?   b = null;
object boxedB = b;         // null reference — no heap allocation, no wrapper
Console.WriteLine(boxedB == null); // True

// Round-trip back:
int?  recovered = (int?)boxedA;  // 42
// int? wrong = (double?)boxedA; // InvalidCastException — was boxed as Int32

This design ensures null round-trips cleanly through object. A null int? assigned to object gives you null back — not some "boxed null" pseudo-object.

Practical null-safety patterns

#nullable enable

// 1. Guard clause:
void Process(string? input)
{
    ArgumentNullException.ThrowIfNull(input);  // .NET 6+ one-liner
    // input is non-null from here
}

// 2. Null object pattern — return a non-null default instead of null:
User GetUser(int id) => db.Users.Find(id) ?? User.Anonymous;

// 3. Optional pattern with records:
record struct Option<T>(T? Value, bool HasValue)
{
    public static Option<T> Some(T value) => new(value, true);
    public static Option<T> None => new(default, false);
}

// 4. Factory method instead of nullable constructor result:
// Instead of: new Parser(input)?.Parse()
// Use: Parser.TryCreate(input, out var parser) ? parser.Parse() : defaultResult

Recap

C# has two distinct null systems. Nullable value types (int? = Nullable<int>) are a runtime CLR feature — a struct with HasValue and Value fields that can represent null for any non-nullable value type. Nullable reference types (C# 8's string? vs string) are a compile-time annotation feature with no runtime overhead — they let the compiler perform null-state flow analysis and warn on potential NullReferenceException before it happens. The ?? operator provides null coalescing, ?. provides safe navigation through object chains, and ??= is lazy assignment that only fires on null. The null-forgiving operator ! suppresses analyser warnings when you have a guarantee the tool cannot infer — use it sparingly. Pattern matching with is not null, is { }, and switch expressions integrates null handling cleanly with the compiler's flow analysis. Enabling <Nullable>enable</Nullable> in new projects from day one catches most null bugs at compile time rather than at runtime.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel