Back to Blog
C#

C# Nullable Usage: Value Types and Reference Types

c# nullable usage: Learn how to use nullable value types and nullable reference types in C#, including null checks, operators, and API boundary annotations.

nullable value typesnullable reference typesC# null handlingnull-coalescing operatornull-conditional operatorNRT warnings
Diagram showing a nullable value type and a nullable reference type with null checks and operators in C#.

C# nullable usage spans two distinct features: nullable value types, available since C# 2, and nullable reference types, introduced in C# 8. Both use the ? suffix, but they behave differently at runtime and compile time. Understanding which one you are working with is the first step to writing null-safe code without suppressing compiler warnings.

The Two Kinds of Nullable in C#

Nullable value types are structs that can also represent null. For example, int? is a Nullable<int> that has a HasValue property. Nullable reference types are a compile-time annotation only; a string? is still a string at runtime, but the compiler tracks whether it might be null and warns when you dereference it without a check.

The distinction matters because int? has a runtime representation that distinguishes null from a missing value, while string? relies on the developer and the compiler to avoid null dereferences. The compiler does not add any runtime checks for nullable reference types.

Declaring and Using Nullable Value Types

A nullable value type is declared with ?:

int? count = null; if (count.HasValue) { Console.WriteLine(count.Value); }

HasValue returns false when the variable is null. Accessing Value when HasValue is false throws InvalidOperationException. To avoid that, use GetValueOrDefault() or the null-coalescing operator:

int result = count ?? 0;

The ?? operator returns the left operand if it is not null, otherwise the right operand. This is the most common way to convert a nullable value type to a non-nullable one.

Nullable Reference Types and Compiler Warnings

Nullable reference types are opt-in. Enable them in the project file:

<Nullable>enable</Nullable>

With this enabled, the compiler treats every reference type as non-nullable by default. You opt into nullability with ?:

string? name = GetName(); Console.WriteLine(name.Length); // warning: possible null dereference

The compiler warns because name could be null. To fix it, check for null first:

if (name is not null) { Console.WriteLine(name.Length); }

The is not null pattern is the preferred way to test for non-null in modern C#. It avoids the == operator, which can be overloaded and may not behave as expected.

Checking for Null Without Losing Information

When you need to distinguish between a null value and a default value, use HasValue for value types and is null for reference types. For example:

int? maybe = GetMaybe(); if (maybe.HasValue) { // use maybe.Value } else { // handle null case }

For reference types, use pattern matching:

string? input = GetInput(); if (input is null) { // handle null } else { // input is non-null here }

The compiler understands these checks and narrows the type in the else branch.

Null-Conditional and Null-Coalescing Operators

The null-conditional operator ?. short-circuits when the left operand is null:

string? name = GetName(); int length = name?.Length ?? 0;

If name is null, name?.Length evaluates to null, and the ?? provides a fallback. This pattern is concise and avoids explicit null checks. It works for both value and reference types, but be careful when the result is a value type: name?.Length returns int?, so you need ?? to get a non-nullable int.

Handling Null in Public APIs and Boundaries

When designing public APIs, decide whether a parameter or return type can be null and annotate it accordingly. A method that accepts a string that may be null should declare string?. A method that returns a value that may be null should return string?. This gives consumers compile-time warnings instead of runtime surprises.

For example:

public string? FindName(int id) { // return null if not found }

Callers must handle the null case. If the method never returns null, declare it as string and the compiler will warn if you try to return null.

Compatibility and Migration Concerns

Nullable reference types are a compile-time feature. They do not change runtime behavior or binary compatibility. Existing libraries that were compiled without annotations are treated as "oblivious" to nullability. The compiler suppresses warnings when interacting with them, which can hide potential null issues. To get full benefit, you need to annotate your own code and, if possible, use libraries that provide nullable annotations.

Migrating an existing codebase to Nullable enabled can produce many warnings. You can enable it gradually using #nullable enable directives in individual files, or set <Nullable>annotations</Nullable> to only generate annotations without warnings. The goal is to make the null contract explicit without breaking existing behavior.

The most important operational consideration is that nullable reference types do not add runtime checks. If you need runtime validation, you must add it explicitly, for example by throwing ArgumentNullException in public methods. The compiler helps you find likely null dereferences, but it does not protect against nulls that come from unannotated external code.

c# nullable usage: Practical Usage and Code Examples | RYUSLOG DEV