📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are nullable value types (Nullable<T>, int?) in C#? How do HasValue, Value, GetValueOrDefault, lifted operators and boxing of a null nullable work?

Nullable value types in C#: the Nullable<T> struct, safe access patterns, lifted arithmetic and comparison operators, three-valued bool logic, boxing behaviour and database mapping.

Junior Theory Beginner boxing csharp lifted-operators nullable nullable-value-types types

🎯 Key points

  • int? is shorthand for Nullable<int>, a struct holding a bool HasValue and a T value
  • Value throws InvalidOperationException when there is no value; GetValueOrDefault and ?? are safe
  • Pattern matching (if (x is int v)) is the cleanest way to test and extract
  • Lifted operators: arithmetic with null gives null; <, >, <= and >= with null give false; == null is true only when empty
  • bool? uses three-valued logic: null & false is false, null | true is true
  • Boxing a null int? gives a null reference; boxing a value gives a boxed int, never a boxed Nullable<int>
  • Use nullable value types for genuinely optional data such as nullable database columns or optional query parameters
  • Nullable value types are a run-time type; nullable reference types (string?) are only compile-time annotations

⏱ 30-second answer

int? is Nullable<int>, a struct that pairs a value with a HasValue flag, so a value type can represent "no value" (a nullable database column, an optional filter). Reading .Value when it is empty throws, so I use ??, GetValueOrDefault() or if (x is int v). Operators are lifted: null + 1 is null, and comparisons like null > 0 are false. Boxing is special: a null int? boxes to a null reference and a non-null one boxes to a plain boxed int. Unlike string?, int? is a real run-time type.

🗣 Full answer (senior)

How I would answer this in an interview

int? is shorthand for Nullable<int>, a struct that wraps a value type with a HasValue flag. It exists because value types cannot be null, but real data often has "no value" — an optional database column, a missing JSON field, "not yet calculated". The interesting parts are the members, lifted operators and the special boxing rules.

1. What Nullable<T> is

public struct Nullable<T> where T : struct
{
    private readonly bool hasValue;
    internal T value;
    public bool HasValue => hasValue;
    public T Value => hasValue ? value : throw new InvalidOperationException("Nullable object must have a value.");
    public T GetValueOrDefault() => value;
    public T GetValueOrDefault(T defaultValue) => hasValue ? value : defaultValue;
}
  • It is itself a value type: no heap allocation, just the value plus a bool (with padding, int? is 8 bytes, long? and decimal? grow similarly).
  • T must be a non-nullable value type, so int?? and string? as Nullable<string> are not allowed.
  • null for an int? means HasValue == false; it is not a null reference.

2. Members and how I use them

Member Behaviour When I use it
HasValue true if a value is present Rarely directly; patterns read better
Value Returns the value or throws InvalidOperationException Only after a check, ideally never
GetValueOrDefault() Value or default(T) — no check, no throw, fastest Hot paths where 0 is a valid fallback
GetValueOrDefault(x) Value or x Explicit fallback
?? x Same as above, idiomatic Most code
is int n / is { } n Test and unwrap in one step Branching
(int)nullable Explicit cast = .Value, throws if null Avoid

Modern idiomatic code:

if (order.DiscountPercent is decimal discount)
    total -= total * discount / 100m;

var retries = settings.MaxRetries ?? 3;

3. Lifted operators

Operators defined on T are automatically lifted to T?:

  • Arithmetic and bitwise (+ - * / % & | ^ << >>): if either operand is null, the result is null. 5 + (int?)null == null.
  • Equality (==, !=): null == null is true; 5 == null is false.
  • Relational (< > <= >=): if either operand is null, the result is false — not null. So null < 5 and null >= 5 are both false, which breaks the usual "if not less then greater-or-equal" logic.
  • bool? with & and | use three-valued (SQL-like) logic: null & false == false, null | true == true.

This mirrors SQL NULL semantics, which is why EF Core can translate them, but it trips people up in C# conditions.

4. Boxing a nullable

The runtime treats Nullable<T> specially when boxing:

  • Boxing an int? with a value produces a boxed int, not a boxed Nullable<int>.
  • Boxing an int? without a value produces a null reference.
int? a = 5;   object oa = a;   // boxed int
int? b = null; object ob = b;  // null
Console.WriteLine(oa.GetType());   // System.Int32
Console.WriteLine(ob is null);     // True
Console.WriteLine(ob?.GetType());  // null - GetType() on ob would throw NRE

And you can unbox a boxed int directly to int?. This makes nullable values behave sensibly in object-typed APIs, but it means you can never observe Nullable<int> via GetType() — use Nullable.GetUnderlyingType(typeof(T)) in reflection.

5. Where they show up in real systems

  • EF Core: a nullable column (int NULL, datetime2 NULL) maps to int?/DateTime?. Making a property non-nullable makes the column NOT NULL in migrations.
  • JSON: System.Text.Json writes null for empty nullables and can ignore them with JsonIgnoreCondition.WhenWritingNull.
  • APIs: query parameters like int? page distinguish "not provided" from 0.
  • Optional updates (PATCH): int? cannot distinguish "not sent" from "set to null" — that needs a wrapper like Optional<T> or JSON Patch.

6. A production example

A reporting endpoint filtered orders with if (order.CompletedAt > cutoff) and an else branch that assumed "not completed after the cutoff" meant "completed before". For orders with CompletedAt == null the comparison was false, so incomplete orders were counted as completed-before-cutoff, inflating a KPI. We rewrote it with explicit patterns (order.CompletedAt is { } done && done > cutoff) and a separate branch for null, and added test cases for null on every nullable comparison in that report.

🔍 What the interviewer is checking

Checks basic correctness with optional data, a daily source of production bugs.

Green flags

  • Knows int? is Nullable<int>, a struct with HasValue.
  • Uses safe access patterns rather than .Value.
  • Explains lifted operators and the comparison-with-null trap.
  • Knows boxing an empty nullable gives null.

Red flags

  • Confuses nullable value types with nullable reference types.
  • Uses sentinel values instead of nullables.
  • Cannot say what null > 0 evaluates to.

📚 Learn in depth

## 1. Memory layout

`Nullable

💻 Code

namespace Types;

public sealed record OrderFilter(decimal? MinTotal, DateOnly? From);

public static class NullableValueTypes
{
    public static void Main()
    {
        int? quantity = null;

        // Safe access
        Console.WriteLine(quantity ?? 1);                      // 1
        Console.WriteLine(quantity.GetValueOrDefault(5));      // 5
        if (quantity is int q) Console.WriteLine(q);           // skipped

        // Lifted operators
        int? sum = quantity + 10;
        Console.WriteLine(sum is null);                        // True
        Console.WriteLine(quantity > 0);                       // False
        Console.WriteLine(quantity <= 0);                      // False as well!

        // bool? three-valued logic
        bool? isVip = null;
        Console.WriteLine((isVip & false) == false);           // True
        Console.WriteLine(isVip == true);                      // False: the safe way to test

        // Boxing
        object boxedNull = quantity;
        object boxedFive = (int?)5;
        Console.WriteLine(boxedNull is null);                  // True
        Console.WriteLine(boxedFive.GetType());                // System.Int32
        int? back = (int?)boxedFive;                           // unbox to int? works

        // Real-world filter with explicit null handling
        var filter = new OrderFilter(null, new DateOnly(2026, 7, 1));
        Console.WriteLine(Describe(filter));
    }

    public static string Describe(OrderFilter f)
    {
        var parts = new List<string>();
        if (f.MinTotal is decimal min && min > 0) parts.Add($"total >= {min}");
        if (f.From is { } from) parts.Add($"from {from:yyyy-MM-dd}");
        return parts.Count == 0 ? "all orders" : string.Join(" and ", parts);
    }
}

⚠ Common mistakes

1. Calling .Value without checking

What happens: order.ShippedAt.Value.ToString("d") on a nullable that is often null. Why it is wrong: throws InvalidOperationException: Nullable object must have a value, which is less obvious in logs than an NRE. Do instead: use is DateTime shipped patterns, ?? fallbacks, or ?. (order.ShippedAt?.ToString("d")).

2. Assuming null comparisons work like normal comparisons

What happens: if (score &gt; 50) Pass(); else Fail(); where score is int?. Why it is wrong: for null, score &gt; 50 is false, so missing scores are treated as failures; !(a &lt; b) is not equivalent to a &gt;= b when nulls are involved. Do instead: handle null explicitly first, then compare.

3. Using GetValueOrDefault() where 0 is meaningful

What happens: discount.GetValueOrDefault() treats "no discount configured" and "0% discount" the same. Why it is wrong: sometimes absence must mean "inherit from parent" or "not yet calculated". Do instead: decide what null means in the domain and handle it explicitly.

4. Calling GetType() on a boxed nullable

What happens: reflection or logging code calls value.GetType() on an object that came from an int?. Why it is wrong: a null nullable boxes to null (NRE), and a non-null one reports Int32, never `Nullable

🏛 Senior insight: trade-offs

Seniors use nullability to model the domain honestly and keep null handling at the edges.

Situation Recommendation
Optional DB column T? property, mapped to NULL column
Optional API input T? parameter, validated at the boundary
"Unknown" versus "zero" matters (e.g. discount) decimal?, never a magic -1
Internal domain logic convert to non-null values early
Tri-state flags consider an enum (Yes, No, Unknown) instead of bool? for clarity
  • Lifted comparison semantics mirror SQL NULL logic; mention this when translating LINQ to SQL with EF Core.
  • Avoid sentinel values (DateTime.MinValue, -1) to mean "missing"; they leak into reports and analytics.
  • In APIs, document whether null and "missing" mean the same thing (important for PATCH semantics).

🔗 Follow-ups the interviewer may ask

  1. When: The candidate contrasts int? with string?. Why: Checks the candidate can separate run-time Nullable<T> from compile-time nullable reference annotations.
    How to answer

    Nullable reference types make nullability part of the declared type for static analysis: string means "should never be null" and string? means "may be null". The compiler tracks a null-state for every expression and warns when a maybe-null value is dereferenced or assigned to a non-nullable. It is compile-time only: there are no run-time checks, and ! merely silences a warning. Attributes like [NotNullWhen(true)] on TryGet methods or [MemberNotNull] on initialisers tell the analyser about contracts it cannot infer. Nulls can still arrive from deserialisation, arrays, default and unannotated code, so I still validate at the boundaries.

  2. When: The candidate mentions GetValueOrDefault or default(T). Why: GetValueOrDefault and empty nullables lead to what default means for every type.
    How to answer

    Every type has a default value: the value you get from all-zero memory. That is null for reference types and nullable types, 0 for numbers, false for bool, and for structs every field zeroed. default(T) gives it explicitly and the default literal infers the type, as in CancellationToken ct = default. Generic code uses it to mean "no value", which is why FirstOrDefault returns 0 rather than null for int. The traps are that default skips struct constructors, gives enums the value 0 even if it is not a defined member, and makes some structs like ImmutableArray<T> unusable until initialised.

↩ Usually asked after

More in Types

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me