Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Подсказка
Впервые сталкиваетесь с типами-ссылками, допускающими значение NULL? Сначала прочтите ссылочные типы, допускающие значения NULL. В этом руководстве предполагается, что вы понимаете разницу между ссылочными типами, не допускающими значения NULL, и ссылочными типами, допускающими значение NULL, а также то, как компилятор отслеживает состояние NULL.
Вы пришли из другого языка? Если вы использовали nullable-типы в Kotlin, strictNullChecks в TypeScript или optionals в Swift, то концептуальная модель здесь та же. Здесь вы узнаете, как выразить намерение проектирования, а не изучать синтаксис.
В этом руководстве вы создадите небольшую библиотеку, которая моделирует опрос. В данных есть два различных сценария, которые можно различать с помощью ссылочных типов, допускающих значение NULL:
-
Вопрос опроса всегда должен присутствовать. Список вопросов и текст каждого вопроса никогда не может быть
null. - Ответ на вопрос может отсутствовать. Респонденты могут отказаться от ответа на некоторые или все вопросы, и модель должна сделать это явным.
Эти правила объявляют с помощью ссылочных типов, не допускающих значение NULL, и ссылочных типов, допускающих значение NULL. Затем компилятор предупреждает, когда поведение кода не соответствует дизайну.
Изучив это руководство, вы:
- Создайте приложение.
- Создайте вопросы опроса.
- Создайте опрос.
- Проверьте требование без значения NULL.
- Создание типов ответов.
- Создание респондентов.
- Создайте один ответ на опрос.
- Создайте набор ответов на опросы.
- Изучите результаты опроса.
Три класса моделируют опрос:
-
SurveyQuestion: один вопрос. Требуется текст и тип вопроса. -
SurveyRun: коллекция вопросов и список респондентов. -
SurveyResponse: ответы одного респондента, которые могут отсутствовать.
Каждый тип использует ссылочные типы, не допускающие значение NULL, для обязательных значений и ссылочных типов, допускающих значение NULL, для отсутствующих значений.
Prerequisites
- Последняя версия .NET SDK
- редактор Visual Studio Code
- C# DevKit
В этом руководстве предполагается, что вы знакомы с C# и Visual Studio или .NET CLI.
Создайте приложение и включите допускающие значение null ссылочные типы
Создайте консольное приложение с именем NullableIntroduction:
dotnet new console -n NullableIntroduction
cd NullableIntroduction
Создание вопросов опроса
Добавьте новый файл с именем SurveyQuestion.cs в проект и замените его содержимое следующим кодом. Текст и тип вопроса не могут иметь значение NULL, поэтому конструктор должен инициализировать оба:
namespace NullableIntroduction;
public enum QuestionType
{
YesNo,
Number,
Text
}
public class SurveyQuestion(QuestionType typeOfQuestion, string text)
{
public string QuestionText { get; } = text;
public QuestionType TypeOfQuestion { get; } = typeOfQuestion;
}
Параметры конструктора являются ненуляемыми ссылочными типами, поэтому компилятор предупреждает вызывающего объекта, если любой из аргументов может быть null.
Создание опроса вопросов
Затем добавьте новый файл с именем SurveyRun.cs в проект и определите SurveyRun класс для хранения списка вопросов:
namespace NullableIntroduction;
public class SurveyRun
{
private List<SurveyQuestion> surveyQuestions = [];
public void AddQuestion(QuestionType type, string question) =>
AddQuestion(new SurveyQuestion(type, question));
public void AddQuestion(SurveyQuestion surveyQuestion) =>
surveyQuestions.Add(surveyQuestion);
}
Поле surveyQuestions имеет тип List<SurveyQuestion>, не допускающий значения NULL. В нем используется выражение коллекции для инициализации пустого списка. Обе перегрузки AddQuestion принимают параметры, не допускающие значения null, поэтому компилятор гарантирует, что вызывающий код не передаёт null.
В Program.cs создайте SurveyRun и добавьте три вопроса:
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, попробуйте добавить следующую строку и перестроить:
surveyRun.AddQuestion(QuestionType.Text, default);
Компилятор выдает предупреждение CS8625, так как default для ссылочного типа имеет значение null, а AddQuestion ожидает значение string, не допускающее NULL. Удалите строку перед продолжением.
Создать типы ответов
Респонденты могут отказаться от опроса, и даже если они участвуют, они могут пропустить отдельные вопросы. Оба вида "отсутствующие" являются допустимыми результатами, и система типов должна сделать их видимыми. Обе формы обозначаются с помощью null.
Добавьте новый файл с именем SurveyResponse.cs в проект и определите SurveyResponse класс. Используйте основной конструктор (параметры, объявленные в самом типе, доступные в теле) для записи всегда обязательных Idэлементов:
namespace NullableIntroduction;
public class SurveyResponse(int id)
{
public int Id { get; } = id;
}
Создание респондентов
Добавьте статический фабричный метод (static метод, который создаёт и возвращает новый экземпляр типа, то есть служит альтернативой прямому вызову конструктора), создающий объекты respondent со случайным идентификатором:
private static readonly Random randomGenerator = new Random();
public static SurveyResponse GetRandomId() => new SurveyResponse(randomGenerator.Next());
Создание одного ответа на опрос
Затем добавьте метод, который запрашивает опрос респонденту. Сохраните ответы в словаре с возможностью значения null, чтобы сам тип показывал, что респондент может отказаться отвечать:
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!";
}
}
Поле surveyResponses равно Dictionary<int, string>?. Если вы разыменовываете поле, не проверив сначала значение на null, компилятор выдаст предупреждение. Внутри AnswerSurvey, компилятор отслеживает, что surveyResponses имеет значение not-null сразу после выражения new, поэтому тело цикла не требует дополнительных проверок.
Создание набора ответов на опросы
Добавьте в SurveyRun метод, который формирует список респондентов, пока не наберётся достаточно согласившихся на участие:
private List<SurveyResponse>? respondents;
public void PerformSurvey(int numberOfRespondents)
{
int respondentsConsenting = 0;
respondents = [];
while (respondentsConsenting < numberOfRespondents)
{
var respondent = SurveyResponse.GetRandomId();
if (respondent.AnswerSurvey(surveyQuestions))
respondentsConsenting++;
respondents.Add(respondent);
}
}
Поле respondents: List<SurveyResponse>? — оно null, пока опрос не запущен.
Звонок PerformSurvey из Main:
surveyRun.PerformSurvey(50);
Изучение результатов опроса
Чтобы сообщать о результатах, экспортируйте несколько вспомогательных функций из SurveyResponse и SurveyRun. В SurveyResponse добавьте члены, реализованные с помощью выражений (члены, определённые с помощью => и одного выражения вместо блока { ... }), которые обрабатывают словарь, допускающий значение null:
public bool AnsweredSurvey => surveyResponses != null;
public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";
AnsweredSurvey проверяет поле с null.
Answer использует ?. оператор условного доступа по null-ссылке (который вычисляется как null, если левая часть имеет значение null, вместо выбрасывания исключения) для безопасного обращения по ссылке, а также ?? оператор объединения с null (который подставляет правый операнд, если левый имеет значение null), чтобы обеспечить запасное значение, отличное от null. Тип возвращаемого метода не допускает stringзначение NULL, поэтому вызывающие элементы не нуждаются в проверке null.
Добавьте SurveyRunэлементы с выражением, предоставляющие список участников и вопросов:
public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());
public ICollection<SurveyQuestion> Questions => surveyQuestions;
public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];
AllParticipants возвращает последовательность, не допускающую значение null, даже если respondents может быть null. Оператор ?? заменяет Enumerable.Empty<SurveyResponse>() , когда поле еще не заполнено. При удалении ?? предложения компилятор предупреждает, что метод может возвращать 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");
}
}
Обратите внимание, что для participant, surveyRun.Questions или surveyRun.GetQuestion(i) не требуется проверка на null. Типы объявляют эти значения как не допускающие значение NULL, поэтому компилятор обрабатывает их как ненулевое значение во всем цикле.
Запустите приложение:
dotnet run
Выходные данные отличаются при каждом запуске, поскольку участники опроса генерируются случайным образом, но в каждой строке либо приводятся ответы участника, либо указывается, что он отказался отвечать.
Conclusion
Готовый пример находится в папке csharp/NullableIntroduction репозитория dotnet/samples . Поэкспериментируйте путем изменения типов, допускающих значение NULL и не допускающих значение NULL. Удаление ? там, где по замыслу допускаются отсутствующие значения, приводит к предупреждениям компилятора, указывающим на каждое место, где отсутствие значения имеет значение.