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


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

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

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

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

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

Вам потребуется настроить компьютер для запуска .NET, включая компилятор C#. Компилятор C# доступен с Visual Studio 2022или пакета SDK для .NET.

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

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

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

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

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

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

<Nullable>enable</Nullable>

До .NET 6 новые проекты не включают элемент Nullable. Начиная с .NET 6, новые проекты включают элемент <Nullable>enable</Nullable> в файл проекта.

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

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

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

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

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

Если вы программируете на C#, вы, возможно, так привыкли к ссылочным типам со значениями 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.

Перейдите на 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?");

Поскольку весь проект находится в контексте с включенным аннотированием nullable, вы получите предупреждения при передаче 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. Задайте каждый вопрос и запишите ответ. Каждый ответ также может быть отсутствующим (или иметь значение null).

Добавьте следующий код в класс 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);
    }
}

В очередной раз ваш выбор допускающего значение NULL 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: