Skip to content

Nullable Types Interview Questions & Answers

15 questions Updated 2026-06-22 Share:

C# nullable types interview questions — Nullable<T>, null-conditional and null-coalescing operators, nullable reference types, and flow analysis.

Read the in-depth guideNull Safety in C#: Nullable Types and Nullable Reference Types(opens in new tab)
15 of 15

A nullable value type (T?) is a wrapper around a value type that adds the ability to represent null in addition to the normal range of values. It is syntactic sugar for Nullable<T>.

int  normal   = 42;
// normal = null; // compile error — int cannot be null

int? nullable = 42;
nullable = null; // fine — Nullable<int> can hold null

// Check before using:
if (nullable.HasValue)
    Console.WriteLine(nullable.Value); // 42

// Safe access with GetValueOrDefault:
int safe = nullable.GetValueOrDefault(-1); // -1 if null, otherwise the value

// Common use: database columns that allow NULL
int? age = GetAgeFromDatabase(userId); // returns null if not set

Nullable<T> is a struct with two fields: bool HasValue and T Value. When HasValue is false, accessing Value throws InvalidOperationException. The T? syntax is shorthand the compiler expands to Nullable<T> automatically.

Rule of thumb: Use int?, DateTime?, bool? (etc.) wherever a value is genuinely optional or may be absent — particularly for database columns, optional form fields, and API responses where the field may be omitted.

There is no difference — they are identical. int? is compiler syntactic sugar that is expanded to System.Nullable<int> before compilation. At runtime, both have exactly the same representation.

int?              a = 5;
Nullable<int>     b = 5;
System.Nullable<int> c = 5;

// All three are the same type:
Console.WriteLine(a.GetType() == b.GetType()); // True
Console.WriteLine(typeof(int?) == typeof(Nullable<int>)); // True

// The ? syntax works for any non-nullable value type:
double?   d = null;
DateTime? dt = null;
Guid?     g  = null;
bool?     flag = null; // three-state logic (true / false / unknown)

The compiler accepts both forms everywhere — in variable declarations, method signatures, and generic type arguments. int? is universally preferred in C# code for brevity.

Rule of thumb: Always write int? not Nullable<int> — the short form is idiomatic C# and is what every IDE and style guide recommends.

?? returns the left operand if it is not null, otherwise returns the right operand. It is a concise way to provide a default value for a potentially null expression.

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

// Without ??: verbose conditional
string display = name != null ? name : "Unknown";

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

// Chains: returns first non-null value
string result = GetFromCache() ?? GetFromDatabase() ?? "default";

// Works with nullable value types:
int? count = GetCount();
int  safe  = count ?? 0;

// Nested in property initialisation:
public string Label => _label ?? (_label = ComputeLabel());

?? short-circuits — if the left side is non-null, the right side is not evaluated. This makes it safe to use with method calls on the right side that have side effects or are expensive.

Rule of thumb: Replace x != null ? x : y with x ?? y. Chain ?? for fallback hierarchies (cache → database → hardcoded default).

?. (the null-conditional or safe-navigation operator) evaluates the left side and, if it is null, returns null instead of throwing NullReferenceException. It short-circuits the entire chain on the first null.

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

// Without ?.: manual null checks
string? city = null;
if (user != null && user.Address != null)
    city = user.Address.City;

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

// With collections:
int? count = user?.Orders?.Count; // null if user or Orders is null

// Invoke a delegate safely:
OnChanged?.Invoke(this, EventArgs.Empty); // skip if delegate is null

// Combine with ??: provide a fallback
string display = user?.Name ?? "Guest";

?. returns null (or default(T) for value types, wrapped in T?) if any step in the chain is null. The ?[] form works the same way for indexers: user?.Orders?[0]?.Item.

Rule of thumb: Use ?. instead of nested null checks when navigating object graphs. Combine with ?? to provide defaults: user?.Profile?.Bio ?? "No bio yet".

Nullable reference types (NRT) is a compile-time opt-in analysis feature (C# 8+) that lets you annotate reference types as nullable (string?) or non-nullable (string). The CLR behaviour is unchanged — it is purely a static analysis layer.

#nullable enable

string  nonNullable = "hello";  // must never be null — compiler warns if you assign null
string? nullable    = null;     // can be null — compiler warns if you dereference without check

// Compiler warns when you use a nullable without null check:
Console.WriteLine(nullable.Length); // CS8602: Dereference of a possibly null reference.

// Safe pattern — use ? or check:
Console.WriteLine(nullable?.Length);
if (nullable != null)
    Console.WriteLine(nullable.Length); // fine — flow analysis narrows to non-null

// Method signatures communicate intent:
string GetDisplayName(string? name) => name ?? "Guest";

Unlike int? (a real runtime Nullable), string? at runtime is just string — the ? annotation exists only in metadata for the compiler. Enabling NRT in an existing codebase typically surfaces many latent null-related bugs.

Rule of thumb: Enable <Nullable>enable</Nullable> in new projects from day one. On existing codebases, enable it per-file with #nullable enable and fix warnings incrementally. Treat all warnings as errors once the codebase is clean.

Enable it project-wide in .csproj, or per-file with a pragma. Without enabling, all reference types are in the "oblivious" nullable state (no warnings either way).

<!-- In .csproj — recommended for all new projects -->
<PropertyGroup>
  <Nullable>enable</Nullable>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>  <!-- optional but recommended -->
</PropertyGroup>
// Per-file (for incremental adoption on existing codebases):
#nullable enable    // enable for this file
// ... code with nullable annotations ...
#nullable disable   // turn off for legacy code below

// Or enable annotations but suppress warnings (useful while migrating):
#nullable enable annotations

After enabling, the compiler emits CS8600–CS8629 warnings for potential null dereferences. Common first-time fixes: add ? to parameters/properties that can be null, add null-checks before dereferences, and use the null-forgiving operator ! for cases where you know better than the analyser.

Rule of thumb: Set <Nullable>enable</Nullable> at solution level in Directory.Build.props for green-field projects. For legacy migrations, enable per-project and fix warnings one at a time — the compiler guides you.

The null-forgiving operator (!) suppresses the compiler's nullable warning for an expression. It tells the analyser "I know this is not null even though you can't prove it." It has no runtime effect — it is erased at compile time.

#nullable enable

string? GetName() => null; // returns null sometimes

// Compiler warns: possible null
string name = GetName(); // CS8600

// Null-forgiving — suppress the warning (use with care!):
string name = GetName()!; // no warning — you're asserting it is not null at runtime

// Legitimate use cases:
// 1. Lazy initialisation pattern where you know it's set before use:
private string _label = null!; // set in constructor flow the analyser can't track

// 2. Test initialisation (xUnit/NUnit [SetUp] methods):
private MyService _service = null!; // assigned in SetUp, analyser can't see that

// 3. After a runtime contract check:
Debug.Assert(value != null);
Console.WriteLine(value!.Length); // analyser doesn't track Debug.Assert

Overusing ! defeats the purpose of nullable reference types. Every ! is a claim you are making — if wrong, it produces a NullReferenceException at runtime with no compiler warning.

Rule of thumb: Use ! only where you have a genuine guarantee the analyser cannot infer — lazy fields, test setup, post-assertion code. If you find yourself using ! often, that's a sign to refactor to avoid the null.

??= assigns the right-hand value to the left-hand variable only if the variable is currently null. It is shorthand for the lazy-initialisation pattern.

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

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

// Works with nullable value types too:
int? count = null;
count ??= ComputeCount(); // only calls ComputeCount() if count is null

// Chaining in property getters (lazy initialisation):
private List<string>? _items;
public List<string> Items => _items ??= new List<string>();
// First access creates the list; subsequent accesses return the existing list

// With method calls — right side evaluated only if left is null:
string? result = null;
result ??= FetchFromServer(); // FetchFromServer only called when result is null

??= is evaluated left-to-right: check if null, assign only if so. The right-hand side is never evaluated when the left side already has a value (short-circuit).

Rule of thumb: Use ??= for simple lazy initialisation of fields and properties. It replaces the if (x == null) x = ... pattern cleanly.

C# pattern matching (C# 7–11) integrates null checking into switch expressions and is expressions, producing cleaner null guards than explicit != null checks.

// is pattern with null check:
object? obj = GetObject();

if (obj is string s)  // only matches if obj is non-null AND is a string
    Console.WriteLine(s.Length);

// Switch expression with null arm:
string Describe(object? value) => value switch
{
    null           => "nothing",
    int n when n < 0 => "negative number",
    int n          => $"positive number {n}",
    string s       => $"string: {s}",
    _              => "something else",
};

// Not-null pattern (C# 9):
if (obj is not null)
    Console.WriteLine(obj.ToString()); // analyser knows non-null in this branch

// Combined with property patterns:
if (user is { Name: not null, Age: > 18 })
    Console.WriteLine("Adult user with a name");

Pattern matching integrates with the nullable flow analysis — after if (x is T t), the compiler knows t is non-null inside the branch. This is cleaner than x != null && x is T t and removes the need for the null-forgiving operator.

Rule of thumb: Use is not null instead of != null for clarity and alignment with the analyser. Use switch expressions with a null arm to make null handling explicit and exhaustive.

When you box a Nullable<T>, the result depends on HasValue:

  • If HasValue is true, the CLR boxes the underlying T value (not the Nullable<T> wrapper).
  • If HasValue is false (null), boxing produces a null reference — not a boxed Nullable<T>.
int?   a = 42;
object boxedA = a;  // boxes to a boxed int (System.Int32), not Nullable<int>
Console.WriteLine(boxedA.GetType().Name); // "Int32" — not "Nullable`1"

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

// Unboxing: cast back to int? or int
int?  unboxed1 = (int?)boxedA;   // 42
int   unboxed2 = (int)boxedA;    // 42 — can unbox to either

// Cannot unbox to the "wrong" nullable:
// double? wrong = (double?)boxedA; // InvalidCastException — was boxed as int

This design means object round-trips through null perfectly. A null nullable assigned to object gives you null back — not a weird "boxed null" object. It also means Nullable<int> itself is never observable as a boxed type at runtime.

Rule of thumb: Avoid boxing nullable value types in hot paths — like all boxing, it causes a heap allocation. The only difference is that a null nullable boxes to a true null (no allocation), so at least that case is free.

Value throws InvalidOperationException if HasValue is false. GetValueOrDefault() returns default(T) (or a supplied fallback) if the nullable is null — no exception.

int? n = null;

// Throws if null:
// int x = n.Value; // InvalidOperationException!

// Safe alternatives:
int a = n.GetValueOrDefault();     // 0 (default int) — no throw
int b = n.GetValueOrDefault(-1);   // -1 — custom default — no throw
int c = n ?? -1;                   // -1 — equivalent using ?? operator

// When you KNOW it is non-null, Value is fine:
int? confirmed = 42;
if (confirmed.HasValue)
    Console.WriteLine(confirmed.Value); // safe — HasValue checked first

// Common pattern: database int column that defaults to 0 if NULL:
int quantity = row.GetNullableInt("qty").GetValueOrDefault(0);

GetValueOrDefault(fallback) is equivalent to nullable ?? fallback — both are valid. The ?? form is usually preferred for brevity. GetValueOrDefault() without an argument is useful when you want the natural zero/false/default rather than inventing a sentinel value.

Rule of thumb: Prefer ?? defaultValue for explicit defaults. Use GetValueOrDefault() (no arg) when you genuinely want the zero value for the type. Never use .Value without first checking .HasValue or using a null guard.

Arithmetic and comparison operators on nullable value types use lifted operators: if either operand is null, the result is null (for arithmetic) or false (for comparisons other than !=). This mirrors SQL's three-value logic.

int? a = 5;
int? b = null;

// Arithmetic — null propagates:
Console.WriteLine(a + b);   // null
Console.WriteLine(a * 2);   // 10  (int? * int → int?)
Console.WriteLine(b + b);   // null

// Comparison — returns bool (not bool?):
Console.WriteLine(a > 3);   // True
Console.WriteLine(b > 3);   // False — not null, just false
Console.WriteLine(b == null); // True — use == null to detect null

// Equality is special: null == null is True, null == non-null is False
int? x = null, y = null;
Console.WriteLine(x == y);  // True

// Pitfall — null comparisons that always return false:
// if (b > 0) { ... }  — silently skipped when b is null
// Always guard: if (b.HasValue && b.Value > 0) { ... }
//          or: if (b > 0 == true) { ... }  — explicit but verbose

The == and != operators are special-cased: null == null returns true. All other comparisons (<, >, <=, >=) return false when either side is null.

Rule of thumb: Treat null propagation in nullable arithmetic like SQL NULLs. Before performing a meaningful comparison or calculation, check HasValue or use ?? to substitute a concrete value.

Nullable types work naturally in ternary and switch expressions, but you must be careful that the compiler can infer a common result type and that null arms are handled.

int? score = GetScore(); // may be null

// Ternary — result is string (non-nullable) in both arms:
string label = score.HasValue
    ? score.Value >= 60 ? "Pass" : "Fail"
    : "Not taken";

// Same with ?? and ?. (often cleaner):
string label2 = score switch
{
    null       => "Not taken",
    >= 90      => "Distinction",
    >= 60      => "Pass",
    _          => "Fail",
};

// Nullable bool — three-state switch:
bool? isConfirmed = GetConfirmation();
string status = isConfirmed switch
{
    true  => "Confirmed",
    false => "Rejected",
    null  => "Pending",
};

// Warning: ternary with mismatched nullable/non-nullable types:
int? a = null;
// var r = condition ? a : 0; // r is int? because one arm is int?

Switch expressions are the cleanest tool for handling nullable types with multiple branches: they force you to be explicit about the null arm, and the compiler warns if a case is missing.

Rule of thumb: Use a null arm in switch expressions when the input is nullable. Prefer switch expressions over chains of ternaries for three or more outcomes — they are easier to read and exhaustiveness is checked at compile time.

These are two different concepts that are easy to conflate. A nullable collection (List<int>?) is a collection reference that can be null. A collection of nullable elements (List<int?>) is a non-null collection that can hold null int entries.

// Nullable collection — the LIST reference itself may be null:
List<int>? maybeList = null;
maybeList?.Add(1);           // safe — does nothing if list is null
int count = maybeList?.Count ?? 0; // 0

// Collection of nullable ints — the list exists but elements may be null:
List<int?> withNulls = new List<int?> { 1, null, 3 };
foreach (int? item in withNulls)
    Console.WriteLine(item ?? -1); // -1 for the null slot

// Common confusion: filtering nulls out of a nullable element list:
List<int> nonNulls = withNulls
    .Where(x => x.HasValue)
    .Select(x => x!.Value)   // safe: HasValue checked
    .ToList();                // { 1, 3 }

// Or more cleanly with OfType<int>():
List<int> clean = withNulls.OfType<int>().ToList(); // { 1, 3 }

In APIs and data models: use a nullable collection (IEnumerable<T>?) when the absence of a list is meaningful (not yet loaded, not applicable). Use IEnumerable<T?> when the list itself is always present but individual items may be absent.

Rule of thumb: Prefer returning an empty collection over a null collection — it simplifies callers. Reserve List<T>? for cases where null genuinely means "not yet fetched" or "not applicable," distinct from an empty result.

Nullable flow analysis (C# 8+) is the compiler's ability to track the null state of a variable through control-flow paths — narrowing its inferred nullability based on checks, assignments, and pattern matches. This eliminates false positives and enables richer diagnostics.

#nullable enable

string? GetName() => null;

void Example(string? name)
{
    // Before check: 'name' is maybe-null — CS8602 if you use it
    // Console.WriteLine(name.Length); // Warning

    if (name == null) return;          // early return

    // After null check: flow analysis knows name is non-null here
    Console.WriteLine(name.Length);    // no warning — narrowed to string

    // Pattern matching also narrows:
    object? obj = GetObject();
    if (obj is string s)
        Console.WriteLine(s.ToUpper()); // s is non-null string here

    // ?? assignment narrows too:
    string resolved = name ?? "default";
    Console.WriteLine(resolved.Length); // no warning — string, not string?
}

// Limitation: the analyser does not cross method boundaries
void DoCheck(string? s) { if (s == null) throw new ArgumentNullException(); }
void Use(string? s)
{
    DoCheck(s);
    // Console.WriteLine(s.Length); // still warns — analyser can't see into DoCheck
    // Fix: use ArgumentNullException.ThrowIfNull(s); which the analyser understands
}

Rule of thumb: Write code that guides the flow analyser: early returns, guard clauses, and is not null checks. Use ArgumentNullException.ThrowIfNull() instead of custom guards so the analyser recognises the null-check pattern.

More ways to practice

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

or
Join our WhatsApp Channel