C# Nullable Int Usage: Syntax, Checks, and Pitfalls
Learn how to declare, check, and safely read nullable int values in C#. Covers int? syntax, HasValue checks, safe retrieval, common pitfalls, and performance characteristics.
Understanding nullable integers in C# starts with the int? syntax, which is a shorthand for Nullable<int>. A nullable int is a value type that can also hold null, making it useful when a variable legitimately has no value—such as a database column that permits NULL or an optional input parameter. Unlike reference types, int? is a struct, so it does not incur heap allocation when used directly.
Declaring and Assigning a Nullable Int
You can declare a nullable int in several ways, all equivalent:
int? a = null; Nullable<int> b = 10; int? c = default; // null
The default value of int? is null, not zero. Assigning a regular int to an int? is an implicit conversion; the value is wrapped in the nullable structure. Assigning null sets the HasValue property to false.
A common mistake is assuming int? behaves like an int with a special sentinel. It does not. The underlying storage is a struct containing both an int value and a boolean flag, but you rarely interact with those fields directly.
Checking Whether a Nullable Int Has a Value
Before reading the value, you must check if it exists. The HasValue property returns true when the nullable contains a non-null value. The Value property returns the underlying int, but throws InvalidOperationException if HasValue is false.
int? maybe = GetOptionalNumber(); if (maybe.HasValue) { Console.WriteLine(maybe.Value); } else { Console.WriteLine("No value"); }
Since C# 7, pattern matching offers a cleaner way:
if (maybe is int number) { Console.WriteLine(number); }
The pattern is int only matches when the nullable has a value, and it extracts the underlying int into a new variable. This avoids the separate HasValue check and is often more readable.
Safely Retrieving the Underlying Value
Calling Value without checking is a frequent source of runtime exceptions. To retrieve the value safely, use GetValueOrDefault() or the null-coalescing operator ??.
int result = maybe.GetValueOrDefault(); // 0 if null int result2 = maybe ?? 0; // 0 if null
GetValueOrDefault() accepts an optional parameter to specify a custom fallback:
int result3 = maybe.GetValueOrDefault(-1);
Both approaches are equivalent for simple fallback logic. The ?? operator is more general because it works with any nullable type and can chain multiple fallbacks.
Nullable Int in Expressions and Conversions
Arithmetic and comparison operators exhibit lifting behavior. When you apply an operator to two nullable ints, the result is also nullable. If either operand is null, the result is null.
int? x = 5; int? y = null; int? sum = x + y; // null
Relational operators return bool, not bool?. If either operand is null, the comparison evaluates to false, except for == and !=, which compare the HasValue and Value properties.
int? a = 5; int? b = null; bool equal = (a == b); // false bool less = (a < b); // false
When converting a nullable int to a non-nullable type, you must explicitly handle the null case. A direct cast throws an exception if the value is null:
int plain = (int)maybe; // InvalidOperationException if maybe is null
Use ?? or GetValueOrDefault() instead.
Common Pitfalls When Using Nullable Int
One recurring mistake is comparing int? to a literal null using == and expecting it to behave like a reference comparison. It works, but only because Nullable<T> overrides Equals and implements ==. However, mixing nullable and non-nullable operands can lead to subtle errors. For example:
int? maybe = null; if (maybe == null) // true { }
This is fine, but avoid using Value without checking HasValue in a multi-threaded context. The value could change between the check and the access. Although Nullable<T> is immutable, the variable itself can be reassigned. If you need a consistent snapshot, copy it to a local first.
Another pitfall is boxing. When you cast int? to object, the result is either null or a boxed int, not a boxed Nullable<int>. This can cause unexpected behavior in generic collections or reflection code.
int? maybe = 5; object boxed = maybe; // boxed int, not Nullable<int>
Performance and Memory Characteristics
Nullable<int> is a struct, so using it as a local variable does not by itself cause a heap allocation. The size is roughly the size of an int plus a boolean flag, so it is slightly larger than a plain int but still small.
Boxing is the main performance concern. If you store int? in a non-generic collection like ArrayList, it gets boxed. In generic collections like List<int?>, no boxing occurs because the type argument is a struct. Prefer generic collections when you need to store many nullable ints.
The lifting behavior is implemented with HasValue checks before the operation, so it adds a little work compared with plain int arithmetic. In most applications the difference is negligible, but if this code is on a hot path, measure it rather than assuming.
When to Choose int? Over Alternatives
Use int? when the absence of a value is a valid state, such as an optional age field or a database column that allows NULL. For scenarios where you need to distinguish between "not set" and "zero", a nullable int is clearer than using a sentinel like -1 or int.MinValue.
If you are working with legacy code that uses a sentinel, consider refactoring to int? to improve type safety and reduce the chance of accidentally using the sentinel as a real value. For performance-critical code that runs millions of times, measure both approaches; the nullable struct is generally efficient, but a custom struct with a flag might be more explicit if you need to avoid the lifting behavior.
When you need to represent a range of integers plus a special "unknown" state, int? is the idiomatic choice. If you need more than one special state, a custom enum or a wrapper class may be more appropriate, but for most cases int? keeps the code simple and self-documenting.