Value types normally can't be null — int x = null; doesn't compile. Nullable<T> (written as T?) adds that capability back for specific cases where "no value" is a real, distinct state from any actual value:
int? maybeAge = null;
if (maybeAge.HasValue) {
Console.WriteLine(maybeAge.Value);
} else {
Console.WriteLine("Unknown");
}
// The null-coalescing operator is the common idiomatic shorthand for "or a default"
int age = maybeAge ?? 0;
Under the hood, int? is a real struct — Nullable<int>, with exactly two fields: the wrapped value, and a bool flag for whether it's actually present. Calling .Value when HasValue is false throws an InvalidOperationException at runtime — a genuinely common bug when a nullable value is dereferenced without checking it first, functionally equivalent to a null-reference exception but on a value type.
This is distinct from nullable reference types (string? with <Nullable>enable</Nullable> turned on in the project file), which is a compile-time-only annotation and analysis feature that helps the compiler warn about potential null-reference bugs on reference types — it doesn't change runtime behavior the way Nullable<T> does for value types, and a string? that's actually null at runtime doesn't throw anything by itself until something tries to dereference it.