Поделиться через


Руководство: Четкая передача замысла проектирования с помощью ссылочных типов, допускающих значение NULL и не допускающих его

Ссылочные типы, допускающие значение NULL, дополняют ссылочные типы так же, как и типы значений, допускающие значение NULL. Переменная объявляется ссылочным типом, допускающим значение NULL, путем добавления ? к типу. Например, string? представляет string, который может принимать значение NULL. Эти новые типы можно использовать для более четкого выражения намерения разработки: некоторые переменные должны всегда иметь значение , а другие могут пропустить значение.

В этом руководстве вы узнаете, как:

  • Добавьте ссылочные типы, допускающие и не допускающие значение NULL, в вашу разработку
  • Включите проверки на Nullable Reference Types по всему вашему коду.
  • Напишите код, в котором компилятор применяет эти решения по проектированию.
  • Используйте возможность ссылок, допускающих значение NULL, в собственных проектах

Необходимые условия

В этом руководстве предполагается, что вы знакомы с C# и .NET, включая Visual Studio или .NET CLI.

Включите ссылочные типы, допускающие значение NULL, в ваши проекты

В этом руководстве вы создадите библиотеку, которая моделирует опрос. В коде используются как типы ссылок, допускающие значение NULL, так и типы ссылок, не допускающие значения NULL, для представления реальных концепций. Вопросы опроса никогда не могут иметь значение NULL. Респондент может предпочесть не отвечать на вопрос. Ответы могут быть null в этом случае.

Код, который вы пишете для этого примера, выражает это намерение, и компилятор применяет это намерение.

Создайте приложение и включите допускающие значение null ссылочные типы

Создайте консольное приложение в Visual Studio или из командной строки с помощью dotnet new console. Присвойите приложению имя NullableIntroduction. После создания приложения необходимо указать, что весь проект компилируется в контексте аннотаций, где включена поддержка [nullable]. Откройте файл .csproj и добавьте элемент Nullable в элемент PropertyGroup. Задайте для его значения значение enable. Необходимо выбрать функцию ссылочных типов, допускающих значение NULL , в проектах, созданных до C# 11 / .NET 7. После включения функции существующие объявления ссылочных переменных становятся непустыми ссылочными типами. Хотя это решение помогает найти проблемы, в которых существующий код может не иметь надлежащих проверок null, он может не точно отражать исходное намерение проектирования:

<Nullable>enable</Nullable>

Проектирование типов для приложения

Для этого приложения опроса требуется создать следующие классы:

  • Класс, который моделирует список вопросов.
  • Класс, который моделирует список людей, с которыми связались для опроса.
  • Класс, который моделирует ответы человека, прошедшего опрос.

Эти типы используют как допускающие значение NULL, так и не допускающие значение NULL ссылочные типы, чтобы указать, какие члены являются обязательными, а какие — необязательными. Ссылочные типы, допускающие значение NULL, четко сообщают о намерении разработки:

  • Вопросы, которые являются частью опроса, никогда не могут быть пустыми: не имеет смысла задавать пустой вопрос.
  • Респонденты никогда не могут быть пустыми. Вы хотите отслеживать людей, с которыми вы связались, даже респонденты, которые отказались участвовать.
  • Любой ответ на вопрос может иметь значение NULL. Респонденты могут отказаться от ответа на некоторые или все вопросы.

Вы могли настолько привыкнуть к ссылочным типам, которые допускают значения null, что вы можете упустить другие возможности для объявления экземпляров, не допускающих NULL.

  • Коллекция вопросов должна быть ненулевой.
  • Коллекция респондентов должна быть ненулевой.

При написании кода вы увидите, что тип ссылок, не допускающий значения NULL, по умолчанию позволяет избежать распространенных ошибок, которые могут привести к NullReferenceException. Один из уроков из этого руководства заключается в том, что вы приняли решения о том, какие переменные могли или не могли быть null. Язык не предоставлял синтаксис для выражения этих решений. Теперь да.

Построенное приложение делает следующие шаги.

  1. Создает опрос и добавляет в него вопросы.
  2. Создает псевдослучайный набор респондентов для опроса.
  3. Связывается с респондентами до тех пор, пока размер завершенного опроса не достигнет целевого числа.
  4. Записывает важную статистику по ответам на опросы.

Создайте опрос с nullable и non-nullable ссылочными типами.

Первый код, который вы пишете, создает опрос. Вы пишете классы для моделирования вопроса опроса и выполнения опроса. В опросе имеется три типа вопросов, отличающихся форматом ответа: Да/Нет, ответы на номера и текстовые ответы. Создайте класс public SurveyQuestion:

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Компилятор интерпретирует каждое объявление переменной ссылочного типа как тип, не допускающий значение NULL, для кода в контексте аннотации с включенной поддержкой nullable. Вы можете увидеть первое предупреждение, добавив свойства для текста вопроса и тип вопроса, как показано в следующем коде:

namespace NullableIntroduction
{
    public enum QuestionType
    {
        YesNo,
        Number,
        Text
    }

    public class SurveyQuestion
    {
        public string QuestionText { get; }
        public QuestionType TypeOfQuestion { get; }
    }
}

Так как вы не инициализировали QuestionText, компилятор выдает предупреждение о том, что ненулевое свойство не было инициализировано. Ваш проект требует, чтобы текст вопроса не был null, поэтому вы добавляете конструктор для его инициализации, а также значения QuestionType. Готовое определение класса выглядит следующим образом:

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);
}

Добавление конструктора удаляет предупреждение. Аргумент конструктора также является ссылочным типом, не допускающим значение NULL, поэтому компилятор не выдает никаких предупреждений.

Затем создайте класс public с именем SurveyRun. Этот класс содержит список объектов и методов SurveyQuestion для добавления вопросов в опрос, как показано в следующем коде:

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);
    }
}

Как и раньше, необходимо инициализировать объект списка в ненулевое значение или компилятор выдает предупреждение. Вторая перегрузка AddQuestion не проверяет значение NULL, так как компилятор помогает применить ненулевой контракт: вы объявили, что эта переменная не может иметь значение NULL. Хотя компилятор предупреждает о потенциальных назначениях NULL, значения null на этапе выполнения все еще возможны. Для общедоступных API рекомендуется добавить проверку аргументов даже для ссылочных типов, не допускающих значение NULL, так как клиентский код может не включать ссылочные типы, допускающие значение NULL, или намеренно передавать значение NULL.

Перейдите на Program.cs в редакторе и замените содержимое Main следующими строками кода:

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, вы получаете предупреждения при передаче null любому методу, ожидающем ненулевого ссылочного типа. Попробуйте добавить следующую строку в Main:

surveyRun.AddQuestion(QuestionType.Text, default);

Создание респондентов и получение ответов на опрос

Затем напишите код, который создает ответы на опрос. Этот процесс включает несколько небольших задач:

  1. Создайте метод, который создает объекты-респонденты. Эти объекты представляют людей, которые попросили заполнить опрос.
  2. Создайте логику, чтобы имитировать вопросы респонденту и собирать ответы или отмечать, что респондент не ответил.
  3. Повторяйте, пока достаточно респондентов не отвечают на опрос.

Вам нужен класс для представления ответа на опрос, поэтому добавьте это сейчас. Включите поддержку Nullable-типа. Добавьте свойство Id и конструктор, который инициализирует его, как показано в следующем коде:

namespace NullableIntroduction
{
    public class SurveyResponse
    {
        public int Id { get; }

        public SurveyResponse(int id) => Id = id;
    }
}

Затем добавьте метод static для создания новых участников, создав случайный идентификатор:

private static readonly Random randomGenerator = new Random();
public static SurveyResponse GetRandomId() => new SurveyResponse(randomGenerator.Next());

Основная ответственность этого класса заключается в создании ответов для участника на вопросы в опросе. Эта ответственность состоит из нескольких шагов.

  1. Попросите принять участие в опросе. Если пользователь не дает согласия, верните незаполненный ответ (или null).
  2. Задайте каждый вопрос и запишите ответ. Каждый ответ также может быть пропущен (или нулевым).

Добавьте следующий код в класс SurveyResponse:

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>?, указывающее, что оно может быть пустым. Вы используете новую функцию языка для объявления намерения разработки, как компилятору, так и любому, кто читает код позже. Если вы когда-либо разыменуете surveyResponses без предварительной проверки значения null, вы получите предупреждение компилятора. В методе AnswerSurvey предупреждение не отображается, так как компилятор может определить, что переменная surveyResponses имеет ненулевое значение в предыдущем коде.

Использование null для обозначения отсутствующих ответов выделяет ключевую точку при работе с ссылочными типами, допускающими значение NULL: ваша цель не в том, чтобы удалить все значения null из программы. Скорее, ваша цель заключается в том, чтобы убедиться, что код, который вы пишете, выражает намерение вашего проекта. Отсутствующие значения — это необходимая концепция для выражения в коде. Значение null является четким способом выражения этих отсутствующих значений. Попытка удалить все значения null приводит к необходимости определить другой способ выражения этих отсутствующих значений без null.

Затем необходимо написать метод PerformSurvey в классе SurveyRun. Добавьте следующий код в класс SurveyRun:

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);
    }
}

Ваш выбор nullable List<SurveyResponse>? снова указывает, что ответ может иметь значение NULL. Это означает, что опрос еще не был предоставлен ни одному респондентам. Обратите внимание, что респонденты добавляются до достаточного согласия.

Последним шагом для запуска опроса является добавление вызова для выполнения опроса в конце метода Main:

surveyRun.PerformSurvey(50);

Изучение ответов на опросы

Последний шаг — отображение результатов опроса. Вы добавляете код ко многим классам, которые вы написали. Этот код демонстрирует важность различия между ссылочными типами, допускающими и не допускающими значение NULL. Начните с того, чтобы добавить в класс SurveyResponse следующие два члена тела выражения:

public bool AnsweredSurvey => surveyResponses != null;
public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";

Так как surveyResponses является ссылочным типом, допускающим значение NULL, необходимо проверить значение NULL перед отменой ссылки на него. Метод Answer возвращает строку, не допускающую значение NULL, поэтому необходимо покрыть случай отсутствия ответа с помощью оператора объединения null.

Затем добавьте в класс SurveyRun следующие три элемента выражения:

public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());
public ICollection<SurveyQuestion> Questions => surveyQuestions;
public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];

Элемент AllParticipants должен учитывать, что переменная respondents может иметь значение NULL, но возвращаемое значение не может быть null. Если изменить это выражение, удалив ?? и последующую пустую последовательность, компилятор предупреждает, что метод может вернуть null, а его сигнатура возвращает ненулевой тип.

Наконец, добавьте следующий цикл в нижней части метода Main:

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 проверок в этом коде, так как вы разработали базовые интерфейсы, чтобы все они возвращали ссылочные типы, не допускающие значения NULL. Статический анализ компилятора помогает обеспечить соблюдение этих контрактов проектирования.

Получение кода

Вы можете получить код для готового руководства из репозитория наших образцов в папке csharp/NullableIntroduction.

Поэкспериментируйте, изменяя объявления типов между допускающими и не допускающими значение NULL ссылочными типами. Увидьте, как это приводит к появлению различных предупреждений, чтобы убедиться, что вы не случайно разыменовали null.

Дальнейшие действия

Узнайте, как использовать ссылочный тип, допускающий значение NULL, при использовании Entity Framework: