Understanding C# Nullable Reference Type Syntax
Learn how to use C# nullable reference type syntax, including enabling nullable annotations, declaring nullable and non-nullable references, using the null-forgiving operator, and handling compiler warnings during migration.
The C# nullable reference type syntax adds a ? suffix to reference types to signal that a variable, parameter, or property may hold a null value. When the feature is enabled, the compiler treats reference type declarations without ? as non-nullable and emits warnings when it detects code that might assign or dereference null incorrectly. This article explains the syntax, how to enable it, and how to use it effectively in existing and new codebases.
Enabling Nullable Reference Types
Nullable reference types are opt-in. If your project was created before the feature existed or with a template that does not enable it, the feature is off until you turn it on. To turn it on for an entire project, set the Nullable element in the .csproj file:
<PropertyGroup> <Nullable>enable</Nullable> </PropertyGroup>
For a single file, use the #nullable enable directive at the top:
#nullable enable
You can also disable the feature for a file with #nullable disable, or restore the project-level setting with #nullable restore. These directives are useful when you are gradually migrating a large codebase and want to enable annotations file by file.
Declaring Nullable and Non-Nullable References
With the feature enabled, a reference type declaration without ? is considered non-nullable. The compiler will warn if you assign a null literal to it or if you pass it a value that could be null.
string name = "Alice"; // non-nullable string? maybeName = null; // nullable
The compiler warns when you dereference a nullable value without a check. For example, calling maybeName.Length directly produces a warning because maybeName could be null. To access the member safely, you use a null check:
if (maybeName is not null) { Console.WriteLine(maybeName.Length); }
The is not null pattern is the recommended way to check for null in modern C#. The classic != null also works, but the pattern syntax is more explicit and works with pattern matching.
The Null-Forgiving Operator
Sometimes you know that a nullable expression is not null, but the compiler cannot prove it. The null-forgiving operator ! suppresses the warning for that expression.
string name = maybeName!;
The ! operator does not change runtime behavior. It only suppresses the nullable warning for that expression. Use it sparingly, because it bypasses the safety the feature provides. If you use ! on a value that is actually null, you will get a NullReferenceException at runtime, just as you would without the feature.
A common use is when you have validated that a value is not null in a separate method or through a pattern that the compiler cannot track. For example, if a method returns a nullable value but you have already checked a condition that guarantees it is not null, you can use ! to avoid a warning.
Understanding Nullable Warnings
The compiler produces warnings when it detects a potential null assignment or dereference. These warnings have codes in the CS86xx range, such as CS8600 for assigning null to a non-nullable variable, and CS8602 for dereferencing a possibly null reference. You can configure how the compiler treats these warnings in the project file:
<PropertyGroup> <WarningsAsErrors>nullable</WarningsAsErrors> </PropertyGroup>
This turns all nullable warnings into compile-time errors, which is useful for teams that want to enforce null safety strictly. However, it can be disruptive when migrating existing code, so many teams start with warnings and gradually address them.
Handling Legacy Code and Interop
When you enable nullable reference types on a codebase that was written without them, you will see many warnings. The compiler assumes that every reference type is non-nullable, but existing code often assigns null without annotations. To manage this, you have several options.
You can add #nullable disable to files that are not yet migrated, or you can use the ? and ! operators to annotate the code as you go. For public APIs, it is important to annotate parameters and return types accurately, because these annotations become part of the contract that other code relies on.
The .NET runtime libraries provide attributes in the System.Diagnostics.CodeAnalysis namespace to give the compiler more information about nullability. For example, [NotNull] indicates that a method's return value is never null, even if the signature says it is nullable. [MaybeNull] indicates that a value may be null even if the signature says it is non-nullable. These attributes are useful when you are wrapping unannotated libraries or implementing interfaces that were defined without nullable annotations.
Common Pitfalls with the Syntax
One common mistake is forgetting to enable the feature. If you use the ? suffix on a reference type while nullable annotations are disabled, the compiler does not treat the annotation as active, and it may also report warning CS8632. Make sure the feature is enabled at the project or file level before relying on the annotation.
Another pitfall is overusing the null-forgiving operator. If you find yourself adding ! to almost every nullable expression, you are probably bypassing the checks that the feature is designed to enforce. Instead, restructure the code to make the null flow explicit, or use a local variable after a null check.
Remember that nullable reference types are a compile-time feature. They do not change the runtime representation of the type. A string? and a string are the same type at runtime. The ? is purely an annotation for the compiler. This is different from nullable value types, where int? is a distinct type that wraps the value.
Compatibility and Maintainability
Because nullable reference types are a compile-time feature, they do not affect binary compatibility. A library compiled with nullable annotations can be consumed by a project that does not have the feature enabled. The annotations are stored in metadata, but they are only used by the compiler when the consuming project also has nullable enabled.
This makes the feature safe to adopt incrementally. You can add annotations to a public API without breaking existing consumers. However, the annotations become part of the API's contract. If you change a parameter from string to string?, you are signaling that null is now allowed, and consuming code with nullable enabled may get new warnings when it passes a value that could be null. Similarly, changing a return type from string? to string communicates that null is no longer a valid result, so callers written to handle null may need to be updated.
When designing new APIs, decide whether null is a valid value for each parameter and return type. Use ? only when null is a meaningful state, not just to avoid warnings. For internal code, the annotations help you catch bugs early, but they require discipline to maintain. The compiler will not catch every null-related bug, but it makes the intent of your code much clearer to other developers.