教程:使用可为空和不可为空引用类型更清晰地表达设计意图Tutorial: Express your design intent more clearly with nullable and non-nullable reference types

C# 8.0 引入了可为空引用类型,它们以与可为空值类型补充值类型相同的方式补充引用类型。C# 8.0 introduces nullable reference types, which 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. 例如,string? 表示可为空的 stringFor 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:

  • 将可为空和不可为空引用类型合并到你的设计中Incorporate nullable and non-nullable reference types into your designs
  • 在整个代码中启用可为空引用类型检查。Enable nullable reference type checks throughout your code.
  • 编写编译器强制执行这些设计决策的代码。Write code where the compiler enforces those design decisions.
  • 在自己的设计中使用可为空引用功能Use the nullable reference feature in your own designs

先决条件Prerequisites

需要将计算机设置为运行 .NET Core,包括 C# 8.0 编译器。You'll need to set up your machine to run .NET Core, including the C# 8.0 compiler. Visual Studio 2019.NET Core 3.0 随附 C# 8.0 编译器。The C# 8.0 compiler is available with Visual Studio 2019, or .NET Core 3.0.

本教程假设你熟悉 C# 和 .NET,包括 Visual Studio 或 .NET Core CLI。This tutorial assumes you're familiar with C# and .NET, including either Visual Studio or the .NET Core CLI.

将可为空引用类型合并到你的设计中Incorporate nullable reference types into your designs

在本教程中,你将构建一个模拟运行调查的库。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. 调查问题决不会为 NULL。The survey questions can never be null. 回应者可能不愿回答某个问题。A respondent might prefer not to answer a question. 在这种情况下响应可能为 nullThe 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 the application and enable nullable reference types

在 Visual Studio 中或使用 dotnet new console 从命令行创建新的控制台应用程序。Create a new console application either in Visual Studio or from the command line using dotnet new console. 命名应用程序 NullableIntroductionName 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. 打开 .csproj 文件,并向 PropertyGroup 元素添加 Nullable 元素 。Open the .csproj file and add a Nullable element to the PropertyGroup element. 将其值设置为 enableSet its value to enable. 必须选择“可为空引用类型”功能,即使在 C# 8.0 项目中也是如此 。You must opt into the nullable reference types feature, even in C# 8.0 projects. 这是因为,一旦启用该功能,现有的引用变量声明将成为不可为空引用类型 。That's because once the feature is turned on, existing reference variable declarations become non-nullable reference types. 尽管该决定将有助于发现现有代码可能不具有适当的 NULL 检查的问题,但它可能无法准确反映你的原始设计意图: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>

设计应用程序的类型Design the types for the application

此调查应用程序需要创建许多类:This survey application requires creating a number of classes:

  • 建模问题列表的类。A class that models the list of questions.
  • 建模为调查所联系的人员列表的类。A class that models a list of people contacted for the survey.
  • 建模来自参加调查人员的答案的类。A class that models the answers from a person that took the survey.

这些类型将使用可为空和不可为空引用类型来表示哪些成员是必需的,哪些成员是可选的。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:

  • 调查中的问题不可为 null:提出空问题没有任何意义。The questions that are part of the survey can never be null: It makes no sense to ask an empty question.
  • 回应者永远不能为 NULL。The respondents can never be null. 你需要跟踪所联系的人员,即便回应者拒绝参与也是如此。You'll want to track people you contacted, even respondents that declined to participate.
  • 对某个问题的任何响应都可能为 NULL。Any response to a question may be null. 回应者可拒绝回答部分或全部问题。Respondents can decline to answer some or all questions.

如果你使用 C# 编程,则可能已经习惯于允许 null 值的引用类型,但你可能错过了其他声明不可为空实例的机会: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:

  • 问题集合应不可为空。The collection of questions should be non-nullable.
  • 回应者集合应不可为空。The collection of respondents should be non-nullable.

在编写代码时,你将看到不可为空引用类型作为引用的默认值,可避免可能导致 NullReferenceException 的常见错误。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. 从本教程得出的一个经验是,你应决定哪些变量可为或不可为 nullOne 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:

  1. 创建调查并向其添加问题。Creates a survey and adds questions to it.
  2. 为调查创建一组伪随机回应者。Creates a pseudo-random set of respondents for the survey.
  3. 联系回应者,直到已完成的调查规模达到目标数量。Contacts respondents until the completed survey size reaches the goal number.
  4. 写出有关调查响应的重要统计数据。Writes out important statistics on the survey responses.

使用可为 null 和不可为 null 引用类型构建调查Build the survey with nullable and non-nullable reference types

你将编写的第一个代码创建调查。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. 创建 public SurveyQuestion 类: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; }
    }
}

因为尚未初始化 QuestionText,所以编译器会发出警告,指出尚未初始化不可为空属性。Because you haven't initialized QuestionText, the compiler issues a warning that a non-nullable property hasn't been initialized. 设计要求问题文本为非空,因此需要添加构造函数来初始化它以及 QuestionType 值。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.

接下来,创建一个名为 SurveyRunpublic 类。Next, create a public class named SurveyRun. 此类包含 SurveyQuestion 对象的列表以及向调查添加问题的方法,如以下代码所示: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. AddQuestion 的第二次重载中没有 NULL 检查,因为不需要进行二次检查:已声明该变量不可为空。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. 其值不可为 nullIts value can't be null.

切换到编辑器中的 Program.cs ,并使用以下代码行替换 Main 的内容: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?");

由于整个项目处于启用的可为空的注释上下文中,因此将 null 传递给任何应为不可为空引用类型的方法时,将收到警告。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. 通过将以下行添加到 Main 进行尝试:Try it by adding the following line to Main:

surveyRun.AddQuestion(QuestionType.Text, default);

创建回应者并获取调查答案Create respondents and get answers to the survey

接下来,编写生成调查答案的代码。Next, write the code that generates answers to the survey. 此过程涉及到多个小型任务:This process involves several small tasks:

  1. 构建一个生成回应者对象的方法。Build a method that generates respondent objects. 这些对象表示要求填写调查的人员。These represent people asked to fill out the survey.
  2. 生成逻辑以模拟向回应者询问问题并收集答案,或者注意到回应者没有回答。Build logic to simulate asking the questions to a respondent and collecting answers or noting that a respondent didn't answer.
  3. 重复以上过程,直到有足够的回应者回答此调查。Repeat until enough respondents have answered the survey.

你需要一个表示调查响应的类,所以现在就添加它。You'll need a class to represent a survey response, so add that now. 启用可为空支持。Enable nullable support. 添加初始化它的 Id 属性和构造函数,如以下代码所示: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;
    }
}

接下来,通过生成随机 ID 添加 static 方法来创建新参与者: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:

  1. 要求参加这项调查。Ask for participation in the survey. 如果此人不同意,则返回缺失(或 NULL)响应。If the person doesn't consent, return a missing (or null) response.
  2. 询问每个问题并记录答案。Ask each question and record the answer. 每个答案也可能会缺失(或 NULL)。Each answer may also be missing (or null).

将以下代码添加到 SurveyResponse 类: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!";
    }
}

调查答案的存储空间为 Dictionary<int, string>?,表示它可能为 NULL。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. 如果在不首先检查是否为 null 值的情况下取消引用 surveyResponses,则会收到编译器警告。If you ever dereference surveyResponses without checking for the null value first, you'll get a compiler warning. 你没有在 AnswerSurvey 方法中收到警告,因为编译器可以确定 surveyResponses 变量已设置为上述非空值。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.

对缺少的答案使用 null 强调了处理可为空引用类型的一个关键点:目标不是从程序中删除所有 null 值。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. null 值是表示这些缺失值的一种明确方法。The null value is a clear way to express those missing values. 尝试删除所有 null 值只会导致定义一些其他方法来在没有 null 的情况下表示缺失值。Trying to remove all null values only leads to defining some other way to express those missing values without null.

接下来,你需要在 SurveyRun 类中编写 PerformSurvey 方法。Next, you need to write the PerformSurvey method in the SurveyRun class. 将下面的代码添加到 SurveyRun 类中: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);
    }
}

同样,你选择的可为空 List<SurveyResponse>? 指示响应可能为 NULL。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.

运行调查的最后一步是添加一个调用,从而可在 Main 方法结束时执行此调查: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);

检查调查响应Examine survey responses

最后一步是显示调查结果。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. 首先将以下两个表达式形式成员添加到 SurveyResponse 类: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";

因为 surveyResponses 是一个不可为空引用,所以在取消引用之前不需要输入任何检查。Because surveyResponses is a nullable reference type, null checks are necessary before de-referencing it. Answer 方法返回不可为空的字符串,因此我们必须使用 null 合并运算符来涵盖缺少答案的情况。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.

接下来,将这三个表达式形式成员添加到 SurveyRun 类: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];

AllParticipants 成员必须考虑 respondents 变量可能为 NULL 但返回值不能为 NULL 的情况。The AllParticipants member must take into account that the respondents variable might be null, but the return value can't be null. 如果通过删除后面的 ?? 和空序列来更改该表达式,则编译器会警告你方法可能返回 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.

最后,在 Main 方法的底部添加以下循环: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");
    }
}

你不需要在此代码中执行任何 null 检查,因为你已设计了基础接口,这样它们均返回不可为空引用类型。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.

获取代码Get the code

你可以从 csharp/NullableIntroduction 文件夹中的示例存储库获取已完成教程的代码。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. 了解如何生成不同的警告以确保不会意外取消引用 nullSee how that generates different warnings to ensure you don't accidentally dereference a null.

后续步骤Next steps

要了解更多信息,请迁移现有应用程序以使用可为空引用类型:Learn more by migrating an existing application to use nullable reference types:

了解如何在使用实体框架时使用可为空引用类型:Learn how to use nullable reference type when using Entity Framework:

Entity Framework Core 基础知识:使用可为空引用类型Entity Framework Core Fundamentals: Working with Nullable Reference Types