Events
17 Mar, 9 pm - 21 Mar, 10 am
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register nowThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Nullable reference types complement reference types the same way nullable value types complement value types. You declare a variable to be a nullable reference type by appending a ?
to the type. For example, string?
represents a nullable string
. You can use these new types to more clearly express your design intent: some variables must always have a value, others may be missing a value.
In this tutorial, you'll learn how to:
You'll need to set up your machine to run .NET, including the C# compiler. The C# compiler is available with Visual Studio 2022, or the .NET SDK.
This tutorial assumes you're familiar with C# and .NET, including either Visual Studio or the .NET CLI.
In this tutorial, you'll build a library that models running a survey. The code uses both nullable reference types and non-nullable reference types to represent the real-world concepts. The survey questions can never be null. A respondent might prefer not to answer a question. The responses might be null
in this case.
The code you'll write for this sample expresses that intent, and the compiler enforces that intent.
Create a new console application either in Visual Studio or from the command line using dotnet new console
. Name the application NullableIntroduction
. Once you've created the application, you'll need to specify that the entire project compiles in an enabled nullable annotation context. Open the .csproj file and add a Nullable
element to the PropertyGroup
element. Set its value to enable
. You must opt in to the nullable reference types feature in projects earlier than C# 11. That's because once the feature is turned on, existing reference variable declarations become non-nullable reference types. While that decision will help find issues where existing code may not have proper null-checks, it may not accurately reflect your original design intent:
<Nullable>enable</Nullable>
Prior to .NET 6, new projects do not include the Nullable
element. Beginning with .NET 6, new projects include the <Nullable>enable</Nullable>
element in the project file.
This survey application requires creating a number of classes:
These types will make use of both nullable and non-nullable reference types to express which members are required and which members are optional. Nullable reference types communicate that design intent clearly:
If you've programmed in C#, you may be so accustomed to reference types that allow null
values that you may have missed other opportunities to declare non-nullable instances:
As you write the code, you'll see that a non-nullable reference type as the default for references avoids common mistakes that could lead to NullReferenceExceptions. One lesson from this tutorial is that you made decisions about which variables could or could not be null
. The language didn't provide syntax to express those decisions. Now it does.
The app you'll build does the following steps:
The first code you'll write creates the survey. You'll write classes to model a survey question and a survey run. Your survey has three types of questions, distinguished by the format of the answer: Yes/No answers, number answers, and text answers. Create a public SurveyQuestion
class:
namespace NullableIntroduction
{
public class SurveyQuestion
{
}
}
The compiler interprets every reference type variable declaration as a non-nullable reference type for code in an enabled nullable annotation context. You can see your first warning by adding properties for the question text and the type of question, as shown in the following code:
namespace NullableIntroduction
{
public enum QuestionType
{
YesNo,
Number,
Text
}
public class SurveyQuestion
{
public string QuestionText { get; }
public QuestionType TypeOfQuestion { get; }
}
}
Because you haven't initialized QuestionText
, the compiler issues a warning that a non-nullable property hasn't been initialized. Your design requires the question text to be non-null, so you add a constructor to initialize it and the QuestionType
value as well. The finished class definition looks like the following code:
namespace NullableIntroduction;
public enum QuestionType
{
YesNo,
Number,
Text
}
public class SurveyQuestion
{
public string QuestionText { get; }
public QuestionType TypeOfQuestion { get; }
public SurveyQuestion(QuestionType typeOfQuestion, string text) =>
(TypeOfQuestion, QuestionText) = (typeOfQuestion, text);
}
Adding the constructor removes the warning. The constructor argument is also a non-nullable reference type, so the compiler doesn't issue any warnings.
Next, create a public
class named SurveyRun
. This class contains a list of SurveyQuestion
objects and methods to add questions to the survey, as shown in the following code:
using System.Collections.Generic;
namespace NullableIntroduction
{
public class SurveyRun
{
private List<SurveyQuestion> surveyQuestions = new List<SurveyQuestion>();
public void AddQuestion(QuestionType type, string question) =>
AddQuestion(new SurveyQuestion(type, question));
public void AddQuestion(SurveyQuestion surveyQuestion) => surveyQuestions.Add(surveyQuestion);
}
}
As before, you must initialize the list object to a non-null value or the compiler issues a warning. There are no null checks in the second overload of AddQuestion
because they aren't needed: You've declared that variable to be non-nullable. Its value can't be null
.
Switch to Program.cs in your editor and replace the contents of Main
with the following lines of code:
var surveyRun = new SurveyRun();
surveyRun.AddQuestion(QuestionType.YesNo, "Has your code ever thrown a NullReferenceException?");
surveyRun.AddQuestion(new SurveyQuestion(QuestionType.Number, "How many times (to the nearest 100) has that happened?"));
surveyRun.AddQuestion(QuestionType.Text, "What is your favorite color?");
Because the entire project is in an enabled nullable annotation context, you'll get warnings when you pass null
to any method expecting a non-nullable reference type. Try it by adding the following line to Main
:
surveyRun.AddQuestion(QuestionType.Text, default);
Next, write the code that generates answers to the survey. This process involves several small tasks:
You'll need a class to represent a survey response, so add that now. Enable nullable support. Add an Id
property and a constructor that initializes it, as shown in the following code:
namespace NullableIntroduction
{
public class SurveyResponse
{
public int Id { get; }
public SurveyResponse(int id) => Id = id;
}
}
Next, add a static
method to create new participants by generating a random ID:
private static readonly Random randomGenerator = new Random();
public static SurveyResponse GetRandomId() => new SurveyResponse(randomGenerator.Next());
The main responsibility of this class is to generate the responses for a participant to the questions in the survey. This responsibility has a few steps:
Add the following code to your SurveyResponse
class:
private Dictionary<int, string>? surveyResponses;
public bool AnswerSurvey(IEnumerable<SurveyQuestion> questions)
{
if (ConsentToSurvey())
{
surveyResponses = new Dictionary<int, string>();
int index = 0;
foreach (var question in questions)
{
var answer = GenerateAnswer(question);
if (answer != null)
{
surveyResponses.Add(index, answer);
}
index++;
}
}
return surveyResponses != null;
}
private bool ConsentToSurvey() => randomGenerator.Next(0, 2) == 1;
private string? GenerateAnswer(SurveyQuestion question)
{
switch (question.TypeOfQuestion)
{
case QuestionType.YesNo:
int n = randomGenerator.Next(-1, 2);
return (n == -1) ? default : (n == 0) ? "No" : "Yes";
case QuestionType.Number:
n = randomGenerator.Next(-30, 101);
return (n < 0) ? default : n.ToString();
case QuestionType.Text:
default:
switch (randomGenerator.Next(0, 5))
{
case 0:
return default;
case 1:
return "Red";
case 2:
return "Green";
case 3:
return "Blue";
}
return "Red. No, Green. Wait.. Blue... AAARGGGGGHHH!";
}
}
The storage for the survey answers is a Dictionary<int, string>?
, indicating that it may be null. You're using the new language feature to declare your design intent, both to the compiler and to anyone reading your code later. If you ever dereference surveyResponses
without checking for the null
value first, you'll get a compiler warning. You don't get a warning in the AnswerSurvey
method because the compiler can determine the surveyResponses
variable was set to a non-null value above.
Using null
for missing answers highlights a key point for working with nullable reference types: your goal isn't to remove all null
values from your program. Rather, your goal is to ensure that the code you write expresses the intent of your design. Missing values are a necessary concept to express in your code. The null
value is a clear way to express those missing values. Trying to remove all null
values only leads to defining some other way to express those missing values without null
.
Next, you need to write the PerformSurvey
method in the SurveyRun
class. Add the following code in the SurveyRun
class:
private List<SurveyResponse>? respondents;
public void PerformSurvey(int numberOfRespondents)
{
int respondentsConsenting = 0;
respondents = new List<SurveyResponse>();
while (respondentsConsenting < numberOfRespondents)
{
var respondent = SurveyResponse.GetRandomId();
if (respondent.AnswerSurvey(surveyQuestions))
respondentsConsenting++;
respondents.Add(respondent);
}
}
Here again, your choice of a nullable List<SurveyResponse>?
indicates the response may be null. That indicates the survey hasn't been given to any respondents yet. Notice that respondents are added until enough have consented.
The last step to run the survey is to add a call to perform the survey at the end of the Main
method:
surveyRun.PerformSurvey(50);
The last step is to display survey results. You'll add code to many of the classes you've written. This code demonstrates the value of distinguishing nullable and non-nullable reference types. Start by adding the following two expression-bodied members to the SurveyResponse
class:
public bool AnsweredSurvey => surveyResponses != null;
public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";
Because surveyResponses
is a nullable reference type, null checks are necessary before de-referencing it. The Answer
method returns a non-nullable string, so we have to cover the case of a missing answer by using the null-coalescing operator.
Next, add these three expression-bodied members to the SurveyRun
class:
public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());
public ICollection<SurveyQuestion> Questions => surveyQuestions;
public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];
The AllParticipants
member must take into account that the respondents
variable might be null, but the return value can't be null. If you change that expression by removing the ??
and the empty sequence that follows, the compiler warns you the method might return null
and its return signature returns a non-nullable type.
Finally, add the following loop at the bottom of the Main
method:
foreach (var participant in surveyRun.AllParticipants)
{
Console.WriteLine($"Participant: {participant.Id}:");
if (participant.AnsweredSurvey)
{
for (int i = 0; i < surveyRun.Questions.Count; i++)
{
var answer = participant.Answer(i);
Console.WriteLine($"\t{surveyRun.GetQuestion(i).QuestionText} : {answer}");
}
}
else
{
Console.WriteLine("\tNo responses");
}
}
You don't need any null
checks in this code because you've designed the underlying interfaces so that they all return non-nullable reference types.
You can get the code for the finished tutorial from our samples repository in the csharp/NullableIntroduction folder.
Experiment by changing the type declarations between nullable and non-nullable reference types. See how that generates different warnings to ensure you don't accidentally dereference a null
.
Learn how to use nullable reference type when using Entity Framework:
.NET feedback
.NET is an open source project. Select a link to provide feedback:
Events
17 Mar, 9 pm - 21 Mar, 10 am
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register nowTraining
Module
Learn coding practices to help prevent the occurrence of NullReferenceException.