This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can trysigning in orchanging directories.
Access to this page requires authorization. You can trychanging directories.
Nullable reference types are a group of features that minimize the likelihood that your code causes the runtime to throwSystem.NullReferenceException. Three features that help you avoid these exceptions, including the ability to explicitly mark a reference type asnullable:
null
before dereferencing it.The compiler tracks thenull-state of every expression in your code at compile time. Thenull-state has one of two values:
null
.null
.Variable annotations determine thenullability of a reference type variable:
null
value or amaybe-null expression to the variable, the compiler issues a warning. Variables that arenon-nullable have a default null-state ofnot-null.null
value or amaybe-null expression to the variable. When the variable's null-state ismaybe-null, the compiler issues a warning if you dereference the variable. The default null-state for the variable ismaybe-null.The rest of this article describes how those three feature areas work to produce warnings when your code mightdereference anull
value. Dereferencing a variable means to access one of its members using the.
(dot) operator, as shown in the following example:
string message = "Hello, World!";int length = message.Length; // dereferencing "message"
When you dereference a variable whose value isnull
, the runtime throws aSystem.NullReferenceException.
Similarly warnings can be produced when[]
notation is used to access a member of an object when the object isnull
:
using System;public class Collection<T>{ private T[] array = new T[100]; public T this[int index] { get => array[index]; set => array[index] = value; }}public static void Main(){ Collection<int> c = default; c[10] = 1; // CS8602: Possible dereference of null}
You'll learn about:
?
suffix is implemented differently for nullable value types and nullable reference types.Finally, you learn known pitfalls for null-state analysis instruct
types and arrays.
You can also explore these concepts in our Learn module onNullable safety in C#.
Null-state analysis tracks thenull-state of references. An expression is eithernot-null ormaybe-null. The compiler determines that a variable isnot-null in two ways:
null
and wasn't assigned since that check.Any variable that the compiler can't determined asnot-null is consideredmaybe-null. The analysis provides warnings in situations where you might accidentally dereference anull
value. The compiler produces warnings based on thenull-state.
null
before dereferencing it.Consider the following example:
string? message = null;// warning: dereference null.Console.WriteLine($"The length of the message is {message.Length}");var originalMessage = message;message = "Hello, World!";// No warning. Analysis determined "message" is not-null.Console.WriteLine($"The length of the message is {message.Length}");// warning!Console.WriteLine(originalMessage.Length);
In the preceding example, the compiler determines thatmessage
ismaybe-null when the first message is printed. There's no warning for the second message. The final line of code produces a warning becauseoriginalMessage
might be null. The following example shows a more practical use for traversing a tree of nodes to the root, processing each node during the traversal:
void FindRoot(Node node, Action<Node> processNode){ for (var current = node; current != null; current = current.Parent) { processNode(current); }}
The previous code doesn't generate any warnings for dereferencing the variablecurrent
. Static analysis determines thatcurrent
is never dereferenced when it'smaybe-null. The variablecurrent
is checked againstnull
beforecurrent.Parent
is accessed, and before passingcurrent
to theProcessNode
action. The previous examples show how the compiler determinesnull-state for local variables when initialized, assigned, or compared tonull
.
The null-state analysis doesn't trace into called methods. As a result, fields initialized in a common helper method called by all constructors might generate a warning with the following message:
Non-nullable property 'name' must contain a non-null value when exiting constructor.
You can address these warnings in one of two ways:Constructor chaining, ornullable attributes on the helper method. The following code shows an example of each. ThePerson
class uses a common constructor called by all other constructors. TheStudent
class has a helper method annotated with theSystem.Diagnostics.CodeAnalysis.MemberNotNullAttribute attribute:
using System.Diagnostics.CodeAnalysis;public class Person{ public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public Person() : this("John", "Doe") { }}public class Student : Person{ public string Major { get; set; } public Student(string firstName, string lastName, string major) : base(firstName, lastName) { SetMajor(major); } public Student(string firstName, string lastName) : base(firstName, lastName) { SetMajor(); } public Student() { SetMajor(); } [MemberNotNull(nameof(Major))] private void SetMajor(string? major = default) { Major = major ?? "Undeclared"; }}
Nullable state analysis and the warnings the compiler generates help you avoid program errors by dereferencingnull
. The article onresolving nullable warnings provides techniques for correcting the warnings most likely seen in your code. The diagnostics produced from null state analysis are warnings only.
The null-state analysis needs hints from developers to understand the semantics of APIs. Some APIs provide null checks, and should change thenull-state of a variable frommaybe-null tonot-null. Other APIs return expressions that arenot-null ormaybe-null depending on thenull-state of the input arguments. For example, consider the following code that displays a message in upper case:
void PrintMessageUpper(string? message){ if (!IsNull(message)) { Console.WriteLine($"{DateTime.Now}: {message.ToUpper()}"); }}bool IsNull(string? s) => s == null;
Based on inspection, any developer would consider this code safe, and shouldn't generate warnings. However the compiler doesn't know thatIsNull
provides a null check and issues a warning for themessage.ToUpper()
statement, consideringmessage
to be amaybe-null variable. Use theNotNullWhen
attribute to fix this warning:
bool IsNull([NotNullWhen(false)] string? s) => s == null;
This attribute informs the compiler, that, ifIsNull
returnsfalse
, the parameters
isn't null. The compiler changes thenull-state ofmessage
tonot-null inside theif (!IsNull(message)) {...}
block. No warnings are issued.
Attributes provide detailed information about the null-state of arguments, return values, and members of the object instance used to invoke a member. The details on each attribute can be found in the language reference article onnullable reference attributes. As of .NET 5, all .NET runtime APIs are annotated. You improve the static analysis by annotating your APIs to provide semantic information about thenull-state of arguments and return values.
Thenull-state analysis provides robust analysis for local variables. The compiler needs more information from you for member variables. The compiler needs more information to set thenull-state of all fields at the opening bracket of a member. Any of the accessible constructors could be used to initialize the object. If a member field might ever be set tonull
, the compiler must assume itsnull-state ismaybe-null at the start of each method.
You use annotations that can declare whether a variable is anullable reference type or anon-nullable reference type. These annotations make important statements about thenull-state for variables:
null
. The compiler issues a warning when code assigns amaybe-null expression to a variable that shouldn't be null.null
reference:null
.null
value and can be assigned the valuenull
in other code.Any non-nullable reference variable has the initialnull-state ofnot-null. Any nullable reference variable has the initialnull-state ofmaybe-null.
Anullable reference type is noted using the same syntax asnullable value types: a?
is appended to the type of the variable. For example, the following variable declaration represents a nullable string variable,name
:
string? name;
When nullable reference types are enabled, any variable where the?
isn't appended to the type name is anon-nullable reference type. That includes all reference type variables in existing code once you enable this feature. However, any implicitly typed local variables (declared usingvar
) arenullable reference types. As the preceding sections showed, static analysis determines thenull-state of local variables to determine if they'remaybe-null before dereferencing it.
Sometimes you must override a warning when you know a variable isn't null, but the compiler determines itsnull-state ismaybe-null. You use thenull-forgiving operator!
following a variable name to force thenull-state to benot-null. For example, if you know thename
variable isn'tnull
but the compiler issues a warning, you can write the following code to override the compiler's analysis:
name!.Length;
Nullable reference types and nullable value types provide a similar semantic concept: A variable can represent a value or object, or that variable might benull
. However, nullable reference types and nullable value types are implemented differently: nullable value types are implemented usingSystem.Nullable<T>, and nullable reference types are implemented by attributes read by the compiler. For example,string?
andstring
are both represented by the same type:System.String. However,int?
andint
are represented bySystem.Nullable<System.Int32>
andSystem.Int32, respectively.
Nullable reference types are a compile time feature. That means it's possible for callers to ignore warnings, intentionally usenull
as an argument to a method expecting a non nullable reference. Library authors should include run-time checks against null argument values. TheArgumentNullException.ThrowIfNull is the preferred option for checking a parameter against null at run time. Furthermore, the runtime behavior of a program making use of nullable annotations is the same if all the nullable annotations, (?
and!
), are removed. Their only purpose is expressing design intent and providing information for null state analysis.
Important
Enabling nullable annotations can change how Entity Framework Core determines if a data member is required. You can learn more details in the article onEntity Framework Core Fundamentals: Working with Nullable Reference Types.
Generics require detailed rules to handleT?
for any type parameterT
. The rules are necessarily detailed because of history and the different implementation for a nullable value type and a nullable reference type.Nullable value types are implemented using theSystem.Nullable<T> struct.Nullable reference types are implemented as type annotations that provide semantic rules to the compiler.
T
is a reference type,T?
references the corresponding nullable reference type. For example, ifT
is astring
, thenT?
is astring?
.T
is a value type,T?
references the same value type,T
. For example, ifT
is anint
, theT?
is also anint
.T
is a nullable reference type,T?
references that same nullable reference type. For example, ifT
is astring?
, thenT?
is also astring?
.T
is a nullable value type,T?
references that same nullable value type. For example, ifT
is aint?
, thenT?
is also aint?
.For return values,T?
is equivalent to[MaybeNull]T
; for argument values,T?
is equivalent to[AllowNull]T
. For more information, see the article onAttributes for null-state analysis in the language reference.
You can specify different behavior usingconstraints:
class
constraint means thatT
must be a non-nullable reference type (for examplestring
). The compiler produces a warning if you use a nullable reference type, such asstring?
forT
.class?
constraint means thatT
must be a reference type, either non-nullable (string
) or a nullable reference type (for examplestring?
). When the type parameter is a nullable reference type, such asstring?
, an expression ofT?
references that same nullable reference type, such asstring?
.notnull
constraint means thatT
must be a non-nullable reference type, or a non-nullable value type. If you use a nullable reference type or a nullable value type for the type parameter, the compiler produces a warning. Furthermore, whenT
is a value type, the return value is that value type, not the corresponding nullable value type.These constraints help provide more information to the compiler on howT
is used. That helps when developers choose the type forT
and provides betternull-state analysis when an instance of the generic type is used.
Thenullable context determines how nullable reference type annotations are handled and what warnings are produced by static null state analysis. The nullable context contains two flags: theannotation setting and thewarning setting.
Both theannotation andwarning settings are disabled by default for existing projects. Starting in .NET 6 (C# 10), both flags are enabled by default fornew projects. The reason for two distinct flags for the nullable context is to make it easier to migrate large projects that predate the introduction of nullable reference types.
For small projects, you can enable nullable reference types, fix warnings, and continue. However, for larger projects and multi-project solutions, that might generate a large number of warnings. You can use pragmas to enable nullable reference types file-by-file as you begin using nullable reference types. The new features that protect against throwing aSystem.NullReferenceException can be disruptive when turned on in an existing codebase:
class
constraint in generics changed to mean a non-nullable reference type.Thenullable annotation context determines the compiler's behavior. There are four combinations for thenullable context settings:
?
suffix to declare a nullable reference type produces a warning.!
, but it has no effect.?
suffix to declare a nullable reference type.?
suffix are non-nullable reference types.null
.null
.?
suffix to declare a nullable reference type produces a warning.?
suffix.!
.null
, or when you assign a maybe-null expression to a non-nullable variable.?
suffix to declare a nullable reference type.?
suffix are non-nullable reference types.!
, but it has no effect.The nullable annotation context and nullable warning context can be set for a project using the<Nullable>
element in your.csproj file. This element configures how the compiler interprets the nullability of types and what warnings are emitted. The following table shows the allowable values and summarizes the contexts they specify.
Context | Dereference warnings | Assignment warnings | Reference types | ? suffix | ! operator |
---|---|---|---|---|---|
disable | Disabled | Disabled | All are nullable | Produces a warning | Has no effect |
enable | Enabled | Enabled | Non-nullable unless declared with? | Declares nullable type | Suppresses warnings for possiblenull assignment |
warnings | Enabled | Not applicable | All are nullable, but members are considerednot-null at opening brace of methods | Produces a warning | Suppresses warnings for possiblenull assignment |
annotations | Disabled | Disabled | Non-nullable unless declared with? | Declares nullable type | Has no effect |
Reference type variables in code compiled in adisabled context arenullable-oblivious. You can assign anull
literal or amaybe-null variable to a variable that isnullable-oblivious. However, the default state of anullable-oblivious variable isnot-null.
You can choose which setting is best for your project:
Example:
<Nullable>enable</Nullable>
You can also use directives to set these same flags anywhere in your source code. These directives are most useful when you're migrating a large codebase.
#nullable enable
: Sets the annotation and warning flags toenable.#nullable disable
: Sets the annotation and warning flags todisable.#nullable restore
: Restores the annotation flag and warning flag to the project settings.#nullable disable warnings
: Set the warning flag todisable.#nullable enable warnings
: Set the warning flag toenable.#nullable restore warnings
: Restores the warning flag to the project settings.#nullable disable annotations
: Set the annotation flag todisable.#nullable enable annotations
: Set the annotation flag toenable.#nullable restore annotations
: Restores the annotation flag to the project settings.For any line of code, you can set any of the following combinations:
Warning flag | Annotation flag | Use |
---|---|---|
project default | project default | Default |
enable | disable | Fix analysis warnings |
enable | project default | Fix analysis warnings |
project default | enable | Add type annotations |
enable | enable | Code already migrated |
disable | enable | Annotate code before fixing warnings |
disable | disable | Adding legacy code to migrated project |
project default | disable | Rarely |
disable | project default | Rarely |
Those nine combinations provide you with fine-grained control over the diagnostics the compiler emits for your code. You can enable more features in any area you're updating, without seeing more warnings you aren't ready to address yet.
Important
The global nullable context does not apply for generated code files. Under either strategy, the nullable context isdisabled for any source file marked as generated. This means any APIs in generated files are not annotated. No nullable warnings are produced for generated files. There are four ways a file is marked as generated:
generated_code = true
in a section that applies to that file.<auto-generated>
or<auto-generated/>
in a comment at the top of the file. It can be on any line in that comment, but the comment block must be the first element in the file.Generators can opt-in using the#nullable
preprocessor directive.
By default, nullable annotation and warning flags aredisabled. That means that your existing code compiles without changes and without generating any new warnings. Beginning with .NET 6, new projects include the<Nullable>enable</Nullable>
element in all project templates, setting these flags toenabled.
These options provide two distinct strategies toupdate an existing codebase to use nullable reference types.
Arrays and structs that contain reference types are known pitfalls in nullable references and the static analysis that determines null safety. In both situations, a non-nullable reference might be initialized tonull
, without generating warnings.
A struct that contains non-nullable reference types allows assigningdefault
for it without any warnings. Consider the following example:
using System;#nullable enablepublic struct Student{ public string FirstName; public string? MiddleName; public string LastName;}public static class Program{ public static void PrintStudent(Student student) { Console.WriteLine($"First name: {student.FirstName.ToUpper()}"); Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}"); Console.WriteLine($"Last name: {student.LastName.ToUpper()}"); } public static void Main() => PrintStudent(default);}
In the preceding example, there's no warning inPrintStudent(default)
while the non-nullable reference typesFirstName
andLastName
are null.
Another more common case is when you deal with generic structs. Consider the following example:
#nullable enablepublic struct S<T>{ public T Prop { get; set; }}public static class Program{ public static void Main() { string s = default(S<string>).Prop; }}
In the preceding example, the propertyProp
isnull
at run time. It's assigned to non-nullable string without any warnings.
Arrays are also a known pitfall in nullable reference types. Consider the following example that doesn't produce any warnings:
using System;#nullable enablepublic static class Program{ public static void Main() { string[] values = new string[10]; string s = values[0]; Console.WriteLine(s.ToUpper()); }}
In the preceding example, the declaration of the array shows it holds non-nullable strings, while its elements are all initialized tonull
. Then, the variables
is assigned anull
value (the first element of the array). Finally, the variables
is dereferenced causing a runtime exception.
Constructor of a class will still call the finalizer, even when there was an exception thrown by that constructor.
The following example demonstrates that behavior:
public class A{ private string _name; private B _b; public A(string name) { ArgumentNullException.ThrowIfNullOrEmpty(name); _name = name; _b = new B(); } ~A() { Dispose(); } public void Dispose() { _b.Dispose(); GC.SuppressFinalize(this); }}public class B: IDisposable{ public void Dispose() { }}public void Main(){ var a = new A(string.Empty);}
In the preceding example, theSystem.NullReferenceException will be thrown when_b.Dispose();
runs, if thename
parameter wasnull
. The call to_b.Dispose();
won't ever throw when the constructor completes successfully. However, there's no warning issued by the compiler, because static analysis can't determine if a method (like a constructor) completes without a runtime exception being thrown.
Was this page helpful?
Was this page helpful?