Back to Blog
C#

C# Non-Nullable Reference Type Syntax

c# non nullable reference type syntax: Learn the C# non-nullable reference type syntax: enabling nullable context, using ? and !, and handling compiler warnings effect...

nullable reference typesC# syntaxnull safetycompiler warnings
Diagram showing nullable and non-nullable reference type annotations in C# code

When you enable nullable reference types in C#, the compiler treats every reference type as non-nullable by default. That means a plain string declaration promises the value is not null, while string? explicitly allows null. Understanding the c# non nullable reference type syntax and how the compiler enforces it is essential for writing null-safe code without losing flexibility.

Enabling the Nullable Context

The nullable context is not on by default in projects created before C# 8. You can enable it at the project level with the <Nullable>enable</Nullable> element in the .csproj file, or per file with the #nullable enable directive. The project-level setting is usually preferable because it applies consistently across the entire codebase.

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> </Project>

When enabled, the compiler emits warnings for code that may dereference a null value, and it uses the syntax you write to track null state across assignments and method calls.

Declaring Nullable and Non-Nullable Reference Types

The core syntax is the ? suffix on a reference type. A declaration without ? is non-nullable; with ? it is nullable. This distinction is purely a compile-time annotation. It does not change the runtime type or add checks.

string nonNullable = "hello"; // compiler assumes not null string? nullable = null; // explicitly allows null

The compiler uses these annotations to analyze flow. For example, if you try to pass nullable to a method that expects a non-nullable string, you get a warning unless you check for null first.

void PrintLength(string text) { Console.WriteLine(text.Length); } string? maybe = GetValue(); if (maybe is not null) { PrintLength(maybe); // allowed after null check }

This syntax also works on array types, generic type parameters, and delegate types, as long as the type is a reference type.

The Null-Forgiving Operator

The null-forgiving operator ! suppresses the compiler's null-state analysis for an expression. It tells the compiler "I know this value is not null, even though you can't prove it." It does not perform a runtime check and does not throw an exception if the value is actually null.

string? name = GetName(); int length = name!.Length; // no warning, but runtime risk if name is null

Use ! sparingly. It is most useful when you have external information the compiler cannot infer, such as a value set by a framework or a field initialized in a method that the compiler cannot see. Overusing it defeats the purpose of nullable analysis.

Handling Warnings in Constructors and Properties

A common warning appears when a non-nullable property is not initialized in the constructor. The compiler cannot guarantee that the property has a value before it is read. You have several options: assign a default value, use a nullable type, or use the required modifier (C# 11+).

public class Person { public string Name { get; set; } = string.Empty; // default public string? Nickname { get; set; } // nullable }

For fields that are set later through a method, you can use the null-forgiving operator at the point of declaration, but that shifts the responsibility to runtime correctness.

public class Service { private string _connectionString; public void Initialize(string connectionString) { _connectionString = connectionString; } public void Connect() { // _connectionString is non-nullable, but compiler sees it as unassigned // Use ! to suppress the warning if you are sure Initialize is always called first. Console.WriteLine(_connectionString!.Length); } }

A cleaner approach is to make the field nullable and check it before use, or restructure so initialization happens in the constructor.

Nullable Annotations and Generic Types

Generic types require special attention. A T? annotation is only allowed when T is known to be a reference type. In an unconstrained generic, T could be a value type, so T? is ambiguous. C# handles this by applying the ? only if the type parameter is constrained to class or notnull.

public T? GetValue<T>(T? input) where T : class { return input; }

For unconstrained generics, you cannot use T? directly. Instead, you can use the [MaybeNull] attribute from System.Diagnostics.CodeAnalysis to indicate that the return value may be null, even though the type is non-nullable.

using System.Diagnostics.CodeAnalysis; public T GetValue<T>() { return default!; // using ! to suppress warning }

This is an advanced area; the compiler's null-state analysis for generics is conservative. Understand the constraints and attributes available before designing generic APIs.

Interoperability with Legacy Code

When you enable the nullable context on a large existing codebase, you will see many warnings. The compiler treats all reference types in legacy code as non-nullable, which may not match reality. To adopt the feature incrementally, you can use #nullable disable in specific files or regions, or use the ? and ! annotations to document your actual intent.

#nullable disable // Legacy code that does not use nullable annotations #nullable restore

Another strategy is to enable the context at the project level but set WarningsAsErrors to false for nullable warnings, so you can fix them over time without breaking the build. The goal is to gradually increase the coverage until the entire codebase is annotated correctly.

Runtime Behavior and Compile-Time Guarantees

The nullable reference type feature has no runtime effect. The annotations are metadata that the compiler reads, and they are removed after compilation. There is no performance overhead, no extra checks, and no change to the generated IL. The guarantees are compile-time only.

This means you cannot rely on the compiler to prevent null reference exceptions at runtime. A value declared as non-nullable can still be null if it comes from an external library, reflection, or a null! expression. The feature is a tool for expressing intent and catching mistakes early, not a runtime safety net.

Because of this, you should combine nullable annotations with defensive runtime checks when interacting with unannotated code or external systems. For example, when deserializing JSON, a property marked non-nullable may still be missing, so you need to validate the data.

Adopting Nullable Reference Types Incrementally

Introducing nullable reference types to an existing project is a gradual process. Start by enabling the context and fixing warnings in the most critical paths, such as public APIs and data models. Use the #nullable directives to isolate legacy code that is not ready. As you add new code, write it with the nullable syntax from the beginning.

A practical approach is to enable the context in a new project and keep it enabled for all new files. For existing files, enable it file by file and address the warnings. Over time, the compiler's analysis becomes more useful because the annotations are accurate.

One common pitfall is using ! to silence warnings without understanding why the warning exists. Each use of ! should be justified. If a value can actually be null, you should use a nullable type and handle the null case explicitly. The ! operator is for cases where you have external knowledge, not for avoiding proper null handling.

Another consideration is the interaction with Nullable attributes like [NotNull] and [MaybeNull]. These attributes give the compiler additional information about the null state of parameters and return values. They are especially useful when you cannot change the signature, such as when implementing an interface or overriding a method from a library that does not use nullable annotations.

By understanding the syntax and the compiler's behavior, you can use nullable reference types to make your code more self-documenting and reduce null-related bugs without adding runtime overhead.

c# non nullable reference type syntax: Practical Usage and C | RYUSLOG DEV