The Traps of Nullable<T> in C#: a Practical Guide with Tiny Examples
Nullable<T> is not a mini reference type and pretending it is will bite you. This guide walks through how nullable value types work at runtime, how boxing really behaves, and what operator lifting does. You will see small, runnable C# snippets that expose common traps and safe patterns. Learn how to handle equality, pattern matching, and generics without surprises.
If I had a nickel for every time I saw int? treated like string?, I could probably fund my coffee habit. Nullable value types look friendly, but under the hood they have their own rules. Treat them like references and they will absolutely prank you in production. Let’s take a few minutes to demystify Nullable<T> with a tour of how it is represented at runtime, what happens during boxing, how operator lifting works, and the common traps that bite even experienced C# devs.
The real shape of Nullable
T? is syntax sugar for System.Nullable<T>, which is a struct with two fields in spirit: a bool HasValue and a T Value. It preserves value semantics while adding an explicit notion of absence.
Before we do anything fancy, let us get our footing.
int? hp = null;Console.WriteLine(hp.HasValue); // FalseConsole.WriteLine(hp.GetValueOrDefault()); // 0
int? shields = 100;Console.WriteLine(shields.HasValue); // TrueConsole.WriteLine(shields.Value); // 100Reading .Value when HasValue is false throws InvalidOperationException, not NullReferenceException. That difference matters when you are debugging.
int? mana = null;// Console.WriteLine(mana.Value); // Would throw InvalidOperationExceptionConsole.WriteLine(mana ?? 50); // Use a safe fallbackRuntime representation and boxing
Here is where mental models usually go sideways. A Nullable<T> is a value type at runtime. When it is boxed, one of two things happens:
- If it has a value, it boxes as a plain
T. - If it is null, the boxed result is a null reference. There is never a boxed
Nullable<T>instance.
Let us see that in action.
object o1 = (int?)42; // Boxes to System.Int32Console.WriteLine(o1.GetType()); // System.Int32
object o2 = (int?)null; // Boxes to null referenceConsole.WriteLine(o2 is null); // TrueUnboxing follows the same rules. You can recover a nullable from a boxed value or from null.
object box = 7; // Boxed intint? maybe = box as int?; // Lifts back to int?Console.WriteLine(maybe.HasValue); // True
object none = null;int? nope = none as int?; // Also fineConsole.WriteLine(nope.HasValue); // FalseThis is why storing int? in List<object> can surprise you later. Non-null values go in as int, null values go in as null. There is never an object whose runtime type is Nullable<int>.
Lifting rules for operators
C# lifts most operators from T to T?. The broad strokes are:
- Arithmetic with any null operand yields null.
- Relational comparisons with a null operand yield false.
- Equality treats two null operands as equal.
int? a = 10;int? b = null;var sum = a + b; // nullvar less = a < b; // falsevar eq1 = a == b; // falsevar eq2 = b == null; // trueFor bool?, you get three valued logic. This is super handy for tri state flags.
bool? doorOpen = null;var safe = doorOpen && true; // nullvar forced = doorOpen ?? false; // false fallbackRemember that ?? returns the left operand when it has a value or the right operand otherwise. This is often safer than reaching for .Value.
int? lives = null;int final = lives ?? 3; // 3int alsoFine = lives.GetValueOrDefault(3); // 3Equality and the object layer
Because non-null T? boxes to T, equality can cross layers you were not expecting.
int? x = 5;int? y = 5;object ox = x; // Boxed intobject oy = y; // Boxed intConsole.WriteLine(ox.Equals(oy)); // True by int.EqualsIf the nullable is null, the boxed value is null. Treat it accordingly.
int? z = null;object oz = z; // null referenceConsole.WriteLine(oz == null); // TrueA subtle footgun is GetValueOrDefault() with no parameter. It returns the default of T when there is no value, which can be indistinguishable from a real value if zero is meaningful.
int? score = null;Console.WriteLine(score.GetValueOrDefault()); // 0, but there was no valueConsole.WriteLine(score == 0); // False, absence is not zeroPrefer an explicit fallback that communicates intent.
int? score2 = null;int displayed = score2 ?? -1; // Make absence visiblePattern matching and nullability checkpoints
Pattern matching makes working with T? much more readable and avoids accidental exceptions.
int? level = 7;if (level is int n) Console.WriteLine($"Level {n}");else Console.WriteLine("No level");The is int n pattern both checks and unwraps in one move. You can use is not null too, which maps directly to HasValue for T?.
int? credits = null;if (credits is not null) Console.WriteLine("You can shop");Generics with value option types
T? works in generics when T is a struct. This gives you an ergonomic option type without bringing in new dependencies.
static T? FirstOrNone<T>(IEnumerable<T> items) where T : struct{ foreach (var it in items) return it; return null;}Using it is straightforward.
var maybeAnswer = FirstOrNone(new[] { 42 });Console.WriteLine(maybeAnswer.HasValue); // TrueIf you need an option over a reference type, use T plus null or a dedicated option type. Nullable<T> only supports struct.
Common traps and simple guardrails
- Do not treat
int?likestring?. Reference nullability is a compile time flow analysis feature.Nullable<T>is a runtime value container. - Avoid
.Valueunless you just checkedHasValue. Prefer??,GetValueOrDefault(x), or pattern matching. - Be mindful when storing
T?inobjectcollections. Non null values become boxedT, nulls becomenull. - Remember arithmetic with any null yields null. Comparison with null yields false. Equality with two nulls yields true.
- Communicate intent with explicit fallbacks so future you will not have to reverse engineer whether zero means none.
Wrap up
Nullable<T> is a precise tool for representing absence in value types. It is not a second kind of reference null. Understand the runtime shape, remember how boxing behaves, lean on lifted operators, and you will sidestep the sneaky bugs.