C# Nullable Value Types
Nullable value types let a C# value type hold either a normal value or null. They matter whenever a value may be unknown, missing, not measured yet, or not applicable, such as an optional age, a database date, or a score that has not been entered. Instead of inventing fake values like -1 or 0, nullable value types let your program say “there is no value” directly.
Overview: How Nullable Value Types Work
Most C# value types cannot normally be null. An int, double, bool, DateTime, enum, or struct variable stores an actual value. If you declare int count; as a local variable, C# will require you to assign an integer before reading it; it will not quietly mean “missing.”
A nullable value type is written by adding ? after the value type name: int?, double?, bool?, DateTime?, or OrderStatus?. This is shorthand for the generic struct Nullable<T>, where T must be a non-nullable value type. For example, int? and Nullable<int> mean the same type.
Internally, Nullable<T> stores two pieces of information: a T value field and a bool flag that says whether a real value is present. The public HasValue property tells you whether the flag is set. The Value property returns the stored value when HasValue is true, but throws InvalidOperationException when the nullable variable is empty.
This design keeps nullable value types type-safe. A nullable integer is still connected to integer behavior: it can participate in numeric operations, comparisons, formatting, and conversions. But the compiler and runtime also preserve the missing-value state. Many operators are lifted for nullable value types, meaning the operator works on the underlying type and usually returns null when one input is null.
Nullable value types are different from nullable reference types. string? is a compiler analysis feature for reference variables. int? is an actual runtime value type built from Nullable<int>. They use similar syntax because both express “may be null,” but they work differently under the hood.
Syntax
int? optionalCount = null;
Nullable<decimal> optionalPrice = 19.99m;
if (optionalCount.HasValue)
{
int count = optionalCount.Value;
}
int safeCount = optionalCount ?? 0;
int? total = optionalCount + 5;
| Syntax | Meaning |
|---|---|
int? x |
Declares a nullable int. It can contain an int value or null. |
Nullable<int> x |
The full generic form. It is equivalent to int?. |
x.HasValue |
Returns true when x contains a real value. |
x.Value |
Returns the contained value, or throws if x is null. |
x ?? fallback |
Returns x when it has a value; otherwise returns the fallback value. |
x.GetValueOrDefault() |
Returns the contained value, or the underlying type’s default value. |
Examples
Representing a Missing Number
using System;
class Program
{
static void Main()
{
int? age = null;
Console.WriteLine($"Has age? {age.HasValue}");
Console.WriteLine($"Display age: {age ?? 0}");
age = 34;
Console.WriteLine($"Has age? {age.HasValue}");
Console.WriteLine($"Actual age: {age.Value}");
}
}
Output:
Has age? False
Display age: 0
Has age? True
Actual age: 34
The variable age starts with no value. HasValue reports false, and the null-coalescing operator ?? provides a display fallback. After assigning 34, age contains a real integer, so reading Value is safe.
Using Nullable Results from Calculations
using System;
using System.Globalization;
class Program
{
static decimal? CalculateDiscount(decimal total)
{
if (total >= 100m)
{
return total * 0.10m;
}
return null;
}
static void Main()
{
decimal orderTotal = 80m;
decimal? discount = CalculateDiscount(orderTotal);
decimal finalTotal = orderTotal - (discount ?? 0m);
Console.WriteLine($"Order total: ${orderTotal.ToString("F2", CultureInfo.InvariantCulture)}");
Console.WriteLine($"Discount: {(discount.HasValue ? "$" + discount.Value.ToString("F2", CultureInfo.InvariantCulture) : "none")}");
Console.WriteLine($"Final total: ${finalTotal.ToString("F2", CultureInfo.InvariantCulture)}");
}
}
Output:
Order total: $80.00
Discount: none
Final total: $80.00
This method returns decimal? because “no discount” is not the same as a discount of zero in every business rule. The caller uses HasValue for display text and ?? 0m for arithmetic. The example formats money with CultureInfo.InvariantCulture so the output is stable regardless of the computer’s regional settings.
Lifted Operators Preserve Missing Values
using System;
class Program
{
static void Main()
{
int? first = 10;
int? second = null;
int? sum = first + second;
Console.WriteLine($"First has value: {first.HasValue}");
Console.WriteLine($"Second has value: {second.HasValue}");
Console.WriteLine($"Sum has value: {sum.HasValue}");
Console.WriteLine($"Fallback sum: {sum ?? -1}");
Console.WriteLine($"Is first greater than 5? {first > 5}");
Console.WriteLine($"Is second greater than 5? {second > 5}");
}
}
Output:
First has value: True
Second has value: False
Sum has value: False
Fallback sum: -1
Is first greater than 5? True
Is second greater than 5? False
The expression first + second uses a lifted addition operator. Since second is null, the result is also null. Relational comparisons such as > return false when a nullable operand has no value, except for special boolean rules discussed later in more advanced lessons.
How Nullable Value Types Work Step by Step
- The compiler sees
int?and rewrites it asSystem.Nullable<int>. - When you assign
null, the nullable struct is created withHasValueset tofalse. - When you assign an
int, the compiler creates a nullable struct withHasValueset totrueand stores the integer. - When you use
HasValue, the program reads the stored flag. - When you use
Value, the program checks the flag. If it isfalse,InvalidOperationExceptionis thrown. - When you use
??, the left side is checked first. If it has a value, that value is used; otherwise the right side is evaluated. - When you use a lifted operator such as
+, the operation happens only when the needed operands have values. Otherwise, the result follows the nullable operator rule, usually producingnullfor arithmetic.
Boxing has one surprising nullable rule. If a nullable value type with HasValue equal to false is boxed to object, the result is a null reference. If it has a value, only the underlying value is boxed. That means object boxed = (int?)5; boxes an int, not a Nullable<int> object.
Common Mistakes
Reading Value Without Checking
int? score = null;
Console.WriteLine(score.Value);
This compiles, but it throws InvalidOperationException at runtime because score does not contain a value. Check HasValue, use pattern matching, or provide a fallback.
int? score = null;
Console.WriteLine(score.HasValue ? score.Value : 0);
Console.WriteLine(score ?? 0);
Output:
0
0
Assuming Nullable Arithmetic Produces Zero
using System;
class Program
{
static void Main()
{
int? quantity = null;
int? total = quantity * 10;
Console.WriteLine(total.HasValue);
}
}
Output:
False
A missing number is not treated as zero. Nullable arithmetic usually propagates null. If your business rule says missing means zero, say that explicitly.
int? quantity = null;
int total = (quantity ?? 0) * 10;
Console.WriteLine(total);
Output:
0
Trying to Make Nullable Nullable
int?? maybeNumber = null;
This does not compile. Nullable<T> only accepts a non-nullable value type as T. Use one nullable layer and model any extra state with a separate enum, object, or result type.
Best Practices
- Use nullable value types when missing data is a real state in the problem domain.
- Do not use magic numbers such as
-1orDateTime.MinValueto mean “unknown” whennullcommunicates the intent better. - Prefer
??for simple fallback values. - Use
HasValue, pattern matching, orGetValueOrDefaultbefore reading the underlying value. - Read
Valueonly after the code has clearly proved that a value exists. - Be explicit about whether
nullshould propagate through calculations or become a default value. - Use nullable return types for operations where “no result” is expected and not exceptional.
- Avoid nullable fields when a value is required for a valid object. Make required data non-nullable and initialize it properly.
- Remember that
T?means different things for value types and reference types, even though the syntax is similar.
Practice Exercises
- Create a
double?temperature variable. Print"not recorded"when it isnull, then assign a value and print it. - Write a method named
FindPassingScorethat returnsint?. Returnnullwhen no score is at least70. - Given
decimal? taxRate, calculate tax for a subtotal. Use0monly when the rule says a missing tax rate means no tax.
Summary
int?is shorthand forNullable<int>, a value type that can represent a value or no value.- Nullable value types store both an underlying value and a
HasValueflag. Valueis safe only whenHasValueistrue.- The null-coalescing operator
??is the usual way to provide a fallback. - Lifted operators let nullable values participate in arithmetic and comparisons while preserving missing data rules.
- Use nullable value types for genuinely optional numbers, dates, booleans, enums, and structs.
- Be deliberate: sometimes
nullshould propagate, and sometimes it should become an explicit default.
