Nullable Value Types
int? holds an int or nothing at all; HasValue, Value and GetValueOrDefault read it, and lifted operators spread the absence.
int? score = null; // the same type as Nullable<int>A value type always holds a value. An int is some whole number, and no number means "we never found out". That is a problem whenever data can genuinely be missing: a quiz score for someone who has not sat the quiz, a delivery date that is not set yet, a sensor that failed to report.
int? solves it. The ? makes a nullable value type: a small wrapper holding either an int or nothing. int? is shorthand for Nullable<int> and the two are the same type, so you can write either and mix them freely. Assigning null to it means absent, which is not the same as 0.
Reading it
HasValue is true when something is there. Value hands it over, and throws InvalidOperationException when there is nothing, so check first. GetValueOrDefault() returns the underlying type's default — 0 for int — when absent, and GetValueOrDefault(x) returns the x you chose. The pattern if (score is int points) checks and unwraps in one step.
What trips people up
Operators are lifted: feed an absent value in and an absent value comes out, so null + 2 is absent rather than 2. Comparisons go further and answer false both ways. And GetValueOrDefault() quietly erases the difference between "absent" and "zero", which was the whole reason you reached for int?.
Example
int? completedLessons = 12; // shorthand for Nullable<int>
int? score = null; // absent, which is not the same as 0
Console.WriteLine(completedLessons.HasValue);
Console.WriteLine(completedLessons.Value); // safe: HasValue is true
Console.WriteLine(score.HasValue);
Console.WriteLine(score.GetValueOrDefault()); // 0, the default for int
Console.WriteLine(score.GetValueOrDefault(-1)); // -1, a default you pick
// Check, then read. score.Value on its own would throw here.
if (score.HasValue)
{
Console.WriteLine(score.Value);
}
else
{
Console.WriteLine("no score recorded");
}
// Arithmetic on an absent value gives an absent result.
int? doubled = score * 2;
Console.WriteLine(doubled.HasValue);
Console.WriteLine(doubled ?? 0);
// Output:
// True
// 12
// False
// 0
// -1
// no score recorded
// False
// 0When to use it
- Model a subscription end date that has not been decided yet.
- Keep "no reading" apart from "a reading of zero" for a sensor.
- Map a database column that allows NULL onto a C# property.
- Carry the result of int.TryParse as a single value instead of a number plus a bool.
More examples
"Parsed, or not parsed" in one value
TryParse writes 0 into its out parameter when it fails, so the number alone cannot tell you whether the text was "0" or nonsense. Folding the result into an int? keeps the two cases apart, and lets you pass one value around instead of a number and a bool that must travel together. Use it when reading optional numbers out of a form, a query string, or a CSV column.
string[] inputs = ["42", "", "abc"];
foreach (string text in inputs)
{
// int.TryParse reports success separately from the number.
// An int? carries both facts in one value.
int? parsed = int.TryParse(text, out int value) ? value : null;
string shown = parsed.HasValue ? parsed.Value.ToString() : "absent";
Console.WriteLine($"'{text}' -> {shown}");
}
// Output:
// '42' -> 42
// '' -> absent
// 'abc' -> absentUnwrapping with a pattern
This is the same work as checking HasValue and then reading Value, with no chance of getting the order wrong. It also gives the unwrapped number its own name and its own plain int type, so the rest of the loop body does no nullable arithmetic. Reach for it any time you are summing, averaging, or formatting a collection where some entries are missing.
int?[] readings = [3, null, 9];
int total = 0;
int missing = 0;
foreach (int? reading in readings)
{
// "is int degrees" matches only when a value is present, and
// hands it over as a plain int in one step.
if (reading is int degrees)
{
total += degrees;
}
else
{
missing++;
}
}
Console.WriteLine($"total: {total}");
Console.WriteLine($"missing: {missing}");
// Output:
// total: 12
// missing: 1An optional field on a type
A plain int here would force a stand-in value such as -1 or 0, and every piece of code touching the field would have to remember which stand-in was chosen. The ? states it in the type instead. This is also how a database column that allows NULL maps into C#, so an entity with optional columns almost always has int? or DateOnly? properties.
// int? on a field says "we do not know", which a plain int cannot say.
Reading[] readings =
[
new Reading("sensor-a", 21),
new Reading("sensor-b", null)
];
foreach (Reading reading in readings)
{
string shown = reading.Celsius.HasValue
? $"{reading.Celsius.Value} C"
: "not reporting";
Console.WriteLine($"{reading.Name}: {shown}");
}
record Reading(string Name, int? Celsius);
// Output:
// sensor-a: 21 C
// sensor-b: not reporting
Discussion