What Are C# Nullable Reference Types?
C# nullable reference types are a set of compile-time features designed to reduce the risk of NullReferenceException in C# applications.
Before nullable reference types, a reference such as string could contain null, and the compiler generally couldn’t warn you when that value might later be dereferenced. Nullable reference types allow developers to explicitly communicate whether a reference is expected to contain a value or whether null is a valid state.
For example:
string name = "Alice";
string? middleName = null;
Here, name is a non-nullable reference type, while middleName is a nullable reference type.
The important thing to understand is that nullable reference types are primarily a compile-time feature. They don’t create a different runtime type and don’t change the runtime behavior of System.String. Instead, the compiler uses annotations and static analysis to warn you about potentially unsafe null operations.
Why Do Nullable Reference Types Matter in C#?
NullReferenceException is one of the most common problems developers encounter when working with reference types.
Consider this code:
string name = GetUserName();
Console.WriteLine(name.Length);
If GetUserName() can return null, accessing name.Length can cause a runtime exception.
Nullable reference types help move the problem from runtime to development time:
string? name = GetUserName();
if (name is not null)
{
Console.WriteLine(name.Length);
}
The ? tells the compiler that name may be null. The null check then tells the compiler that name is safe to use inside the if block.
This approach makes nullability part of your code’s design and gives developers earlier feedback about potentially unsafe code.
How to Enable Nullable Reference Types in C#
Nullable reference types are controlled by the nullable context.
For modern .NET projects, nullable reference types are commonly enabled in the project file:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
You can also enable nullable analysis for an individual source file:
#nullable enable
The project-level <Nullable>enable</Nullable> setting enables both nullable annotations and nullable warnings. Recent .NET project templates already enable nullable reference types by default, while older projects may require you to add the setting manually.
Nullable Context Options
C# provides four nullable-context settings:
disable— nullable analysis and annotations are disabled.enable— nullable annotations and warnings are enabled.warnings— nullable warnings are enabled without enabling nullable annotations.annotations— nullable annotations are enabled without nullable warnings.
For most new or actively maintained applications, enable is the most useful option because it provides both design-time annotations and compiler warnings.
Nullable Reference Types Example
A practical example makes the concept clearer:
public sealed class User
{
public string Username { get; set; } = string.Empty;
public string? DisplayName { get; set; }
}
Now imagine you want to print the display name:
User user = GetUser();
Console.WriteLine(user.DisplayName.Length);
The compiler can warn because DisplayName is declared as string?.
You can safely handle the nullable value with a null check:
if (user.DisplayName is not null)
{
Console.WriteLine(user.DisplayName.Length);
}
Or use the null-conditional operator:
Console.WriteLine(user.DisplayName?.Length);
Or provide a fallback value:
Console.WriteLine(user.DisplayName ?? "Unknown");
The best choice depends on what null means in your application’s domain.
Understanding Null-State Analysis
One of the most important parts of C# nullable reference types is null-state analysis.
The compiler tracks whether an expression is currently considered:
- Not-null
- Maybe-null
For example:
string? message = null;
Console.WriteLine(message.Length);
The compiler knows that message is potentially null, so accessing Length generates a nullable warning.
If you subsequently assign a known non-null value:
message = "Hello";
Console.WriteLine(message.Length);
the compiler can determine that message is no longer potentially null.
C# also performs this analysis across conditional statements, pattern matching, loops, and early returns.
Null Checks Update the Compiler’s Knowledge
Consider:
string? name = GetName();
if (name is not null)
{
Console.WriteLine(name.Length);
}
Inside the if block, the compiler understands that name is not null.
This is one of the biggest advantages of nullable reference types: you don’t have to add unnecessary null checks everywhere. You can write checks where they actually matter, and the compiler can follow the resulting control flow.
The Null-Forgiving Operator !
Sometimes the compiler doesn’t have enough information to determine that a value isn’t null, but you know that it isn’t.
C# provides the null-forgiving operator (!) for these situations:
string? name = GetName();
Console.WriteLine(name!.Length);
The ! tells the compiler:
Treat this expression as non-null.
However, it does not perform a runtime null check and does not make a null value non-null. If name actually is null, the program can still throw NullReferenceException.
For that reason, the null-forgiving operator should be used carefully. Microsoft recommends preferring a real null check, restructuring the code, or providing better nullable information through API annotations when possible.
Avoid Using ! as a Quick Fix
This:
user.Name!.Length
may silence a compiler warning, but it doesn’t necessarily solve the underlying problem.
If Name can genuinely be null, model it as nullable and handle that case. If it should never be null, consider fixing the initialization or API contract instead.
Nullable Reference Types and Method Parameters
Nullable annotations are particularly useful when designing methods.
Suppose a method requires a value:
public void PrintName(string name)
{
Console.WriteLine(name);
}
The method communicates that callers should provide a non-null string.
If null is valid:
public void PrintName(string? name)
{
Console.WriteLine(name ?? "Unknown");
}
Now the method explicitly communicates that null is an acceptable argument.
This makes public APIs easier to understand and allows the compiler to detect incorrect calls.
Nullable Reference Types and Return Values
Nullable annotations also clarify what a method can return.
If a method always returns a value:
public string GetUserName()
{
return "Alice";
}
If the method may not find a result:
public string? FindUserName(int userId)
{
// Return null when the user doesn't exist.
return null;
}
The return type itself now documents the method’s contract.
A caller receiving string? knows that a null check may be required:
string? name = FindUserName(10);
if (name is not null)
{
Console.WriteLine(name);
}
This is one reason nullable reference types are useful for API design, not just for eliminating compiler warnings.
Nullable Reference Types and Properties
Properties should also accurately represent whether null is valid.
For example:
public class Product
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
Here:
Nameis required.Descriptionis optional.
This distinction helps communicate domain rules directly through the type system.
For required members in appropriate designs, C# also provides required members to ensure callers initialize required properties or fields.
Nullable Reference Types Don’t Apply Only to Local Variables
Nullable annotations can be used throughout your API:
public class CustomerService
{
public Customer? FindCustomer(int id)
{
// ...
return null;
}
public void SaveCustomer(Customer customer)
{
// customer is expected to be non-null.
}
}
This creates a clearer contract between the method and its callers.
When nullable reference types are enabled across a project, these contracts can also improve the development experience when consuming APIs that provide nullable annotations.
Common Mistakes When Using C# Nullable Reference Types
1. Adding ? Everywhere
Nullable reference types aren’t intended to eliminate compiler warnings by making every reference nullable.
Instead of:
string? name = "Alice";
use string when null isn’t a valid state:
string name = "Alice";
The goal is to accurately represent your application’s data model.
2. Suppressing Every Warning with !
Using ! repeatedly can hide real problems.
If you have many expressions like:
value!.Property!.Name!
it’s worth investigating whether your API and object initialization are modeled correctly.
3. Ignoring Constructor Initialization
A non-nullable property should generally be initialized before an object is considered ready for use.
For example:
public class User
{
public string Name { get; set; }
}
Depending on the project’s nullable configuration and language version, the compiler can warn that Name isn’t initialized.
A common solution is:
public class User
{
public string Name { get; set; } = string.Empty;
}
or to require the value through a constructor:
public class User(string name)
{
public string Name { get; } = name;
}
4. Assuming Nullable Annotations Guarantee Runtime Safety
Nullable reference types rely on static analysis. They don’t change the CLR’s runtime null behavior.
Code can still receive null through reflection, unsafe code, external systems, incorrectly annotated libraries, serialization scenarios, or other paths the compiler can’t fully reason about.
Nullable analysis reduces risk; it isn’t a replacement for sound runtime validation.
Best Practices for C# Nullable Reference Types
When working with nullable reference types, follow these principles:
Treat nullability as part of your API design
If a method can return null, declare it:
User? FindUser(int id)
If it can’t:
User GetUser(int id)
Prefer correct types over warning suppression
Don’t use ! simply because the compiler warning is inconvenient.
Initialize non-nullable members
Make sure required fields and properties have valid values when an object is ready to use.
Check nullable values before dereferencing
For example:
if (customer is not null)
{
customer.Save();
}
Use nullable annotations consistently
A project is easier to understand when its APIs consistently distinguish required and optional values.
Pay attention to compiler warnings
Nullable warnings are useful feedback about whether your implementation matches your declared design.
C# Nullable Reference Types vs. Nullable Value Types
Nullable reference types are different from nullable value types.
For a value type such as int, you can use:
int? age = null;
This is a nullable value type and is implemented using Nullable<T>.
For a reference type:
string? name = null;
the ? is a nullable reference type annotation. It doesn’t create a new runtime type. Both string and string? refer to System.String at runtime.
This distinction is important when learning C# nullability because the syntax looks similar while the underlying mechanisms are different.
Conclusion
C# nullable reference types provide a powerful way to make nullability explicit in C# applications. By combining annotations such as string?, compiler null-state analysis, and nullable analysis attributes, developers can identify many potential null-related problems before their applications run.
The key concepts are straightforward:
- Use
stringwhen a reference shouldn’t be null. - Use
string?whennullis a valid state. - Enable nullable analysis with
<Nullable>enable</Nullable>. - Check nullable values before dereferencing them.
- Use the null-forgiving operator
!sparingly. - Treat nullable annotations as part of your API design.
- Don’t assume nullable analysis eliminates every possible runtime null.
Used consistently, nullable reference types make C# code easier to reason about and can significantly reduce the risk of NullReferenceException.
For more see Microsoft Learn: Nullable reference types.