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 Nullablestring? 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
HasValueis true, the CLR boxes the underlyingTvalue (not theNullable<T>wrapper). - If
HasValueis false (null), boxing produces a null reference — not a boxedNullable<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 Fundamentals interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.