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 complement reference types the same way nullable value types complement value types. You declare a variable to be anullable reference type by appending a?
to the type. For example,string?
represents a nullablestring
. You can use these new types to more clearly express your design intent: some variablesmust always have a value, othersmay be missing a value.
In this tutorial, you'll learn how to:
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 benull
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 usingdotnet new console
. Name the applicationNullableIntroduction
. Once you've created the application, you'll need to specify that the entire project compiles in an enablednullable annotation context. Open the.csproj file and add aNullable
element to thePropertyGroup
element. Set its value toenable
. You must opt in to thenullable reference types feature in projects earlier than C# 11. That's because once the feature is turned on, existing reference variable declarations becomenon-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 theNullable
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 allownull
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 toNullReferenceExceptions. One lesson from this tutorial is that you made decisions about which variables could or could not benull
. 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 apublic SurveyQuestion
class:
namespace NullableIntroduction{ public class SurveyQuestion { }}
The compiler interprets every reference type variable declaration as anon-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 initializedQuestionText
, 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 theQuestionType
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 apublic
class namedSurveyRun
. This class contains a list ofSurveyQuestion
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 ofAddQuestion
because the compiler helps enforce the non-nullable contract: You've declared that variable to be non-nullable. While the compiler warns about potential null assignments, runtime null values are still possible. For public APIs, consider adding argument validation even for non-nullable reference types, since client code might not have nullable reference types enabled or could intentionally pass null.
Switch toProgram.cs in your editor and replace the contents ofMain
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 passnull
to any method expecting a non-nullable reference type. Try it by adding the following line toMain
:
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 anId
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 astatic
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 yourSurveyResponse
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 aDictionary<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 dereferencesurveyResponses
without checking for thenull
value first, you'll get a compiler warning. You don't get a warning in theAnswerSurvey
method because the compiler can determine thesurveyResponses
variable was set to a non-null value above.
Usingnull
for missing answers highlights a key point for working with nullable reference types: your goal isn't to remove allnull
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. Thenull
value is a clear way to express those missing values. Trying to remove allnull
values only leads to defining some other way to express those missing values withoutnull
.
Next, you need to write thePerformSurvey
method in theSurveyRun
class. Add the following code in theSurveyRun
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 nullableList<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 theMain
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 theSurveyResponse
class:
public bool AnsweredSurvey => surveyResponses != null;public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";
BecausesurveyResponses
is a nullable reference type, null checks are necessary before de-referencing it. TheAnswer
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 theSurveyRun
class:
public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());public ICollection<SurveyQuestion> Questions => surveyQuestions;public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];
TheAllParticipants
member must take into account that therespondents
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 returnnull
and its return signature returns a non-nullable type.
Finally, add the following loop at the bottom of theMain
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 anynull
checks in this code because you've designed the underlying interfaces so that they all return non-nullable reference types. The compiler's static analysis helps ensure these design contracts are followed.
You can get the code for the finished tutorial from oursamples repository in thecsharp/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 anull
.
Learn how to use nullable reference type when using Entity Framework:
Was this page helpful?
Was this page helpful?