Tutorial: Expressar a sua intenção de estrutura de forma mais clara com tipos de referência anuláveis e não anuláveis
Os tipos de referência anuláveis complementam os tipos de referência da mesma forma que os tipos de valores anuláveis complementam os tipos de valor. Declara que uma variável é um tipo de referência anulável ao acrescentar um ?
ao tipo. Por exemplo, string?
representa um nulo string
. Pode utilizar estes novos tipos para expressar mais claramente a sua intenção de estrutura: algumas variáveis têm de ter sempre um valor, outras podem ter um valor em falta.
Neste tutorial, irá aprender a:
- Incorporar tipos de referência anuláveis e não anuláveis nos seus designs
- Ative verificações de tipo de referência anuláveis em todo o código.
- Escreva código onde o compilador imponha essas decisões de design.
- Utilizar a funcionalidade de referência anulável nos seus próprios designs
Pré-requisitos
Terá de configurar o computador para executar o .NET, incluindo o compilador C#. O compilador C# está disponível com o Visual Studio 2022 ou o SDK .NET.
Este tutorial pressupõe que está familiarizado com C# e .NET, incluindo o Visual Studio ou a CLI do .NET.
Incorporar tipos de referência anuláveis nos seus designs
Neste tutorial, vai criar uma biblioteca que modela a execução de um inquérito. O código utiliza tipos de referência anuláveis e tipos de referência não anuláveis para representar os conceitos do mundo real. As perguntas do inquérito nunca podem ser nulas. Um inquirido pode preferir não responder a uma pergunta. Neste caso, as respostas podem ser null
.
O código que irá escrever para este exemplo expressa essa intenção e o compilador impõe essa intenção.
Criar a aplicação e ativar tipos de referência anuláveis
Crie uma nova aplicação de consola no Visual Studio ou a partir da linha de comandos com dotnet new console
. Atribua um nome à aplicação NullableIntroduction
. Depois de criar a aplicação, terá de especificar que todo o projeto é compilado num contexto de anotação anulável ativado. Abra o ficheiro .csproj e adicione um Nullable
elemento ao PropertyGroup
elemento . Defina o respetivo valor como enable
. Tem de optar ativamente por participar na funcionalidade de tipos de referência anuláveis em projetos anteriores ao C# 11. Isto acontece porque, uma vez ativada a funcionalidade, as declarações de variáveis de referência existentes tornam-se tipos de referência não anuláveis. Embora essa decisão ajude a encontrar problemas em que o código existente pode não ter verificações nulas adequadas, pode não refletir com precisão a sua intenção de design original:
<Nullable>enable</Nullable>
Antes do .NET 6, os novos projetos não incluem o Nullable
elemento . A partir do .NET 6, os novos projetos incluem o <Nullable>enable</Nullable>
elemento no ficheiro de projeto.
Estruturar os tipos para a aplicação
Esta aplicação de inquérito requer a criação de várias classes:
- Uma classe que modela a lista de perguntas.
- Uma classe que modela uma lista de pessoas contactadas para o inquérito.
- Uma classe que modela as respostas de uma pessoa que fez o inquérito.
Estes tipos utilizarão tipos de referência anuláveis e não anuláveis para expressar que membros são necessários e quais os membros que são opcionais. Os tipos de referência anuláveis comunicam claramente essa intenção de design:
- As perguntas que fazem parte do inquérito nunca podem ser nulas: não faz sentido fazer uma pergunta vazia.
- Os inquiridos nunca podem ser nulos. Vai querer controlar as pessoas que contactou, mesmo os inquiridos que se recusaram a participar.
- Qualquer resposta a uma pergunta pode ser nula. Os inquiridos podem recusar-se a responder a algumas ou todas as perguntas.
Se tiver programado em C#, poderá estar tão habituado a tipos de referência que permitem null
valores que possa ter perdido outras oportunidades para declarar instâncias não anuláveis:
- A coleção de perguntas deve não ser nulo.
- A coleção de inquiridos não deve ser nulo.
À medida que escreve o código, verá que um tipo de referência não nulo como predefinição para referências evita erros comuns que podem levar a NullReferenceExceptions. Uma lição deste tutorial é que tomou decisões sobre que variáveis podem ou não ser null
. A linguagem não forneceu sintaxe para expressar essas decisões. Agora sim.
A aplicação que vai criar efetua os seguintes passos:
- Cria um inquérito e adiciona perguntas ao mesmo.
- Cria um conjunto pseudo-aleatório de inquiridos para o inquérito.
- Contacta os inquiridos até que o tamanho do inquérito concluído atinja o número do objetivo.
- Escreve estatísticas importantes sobre as respostas do inquérito.
Criar o inquérito com tipos de referência anuláveis e não anuláveis
O primeiro código que vai escrever cria o inquérito. Vai escrever turmas para modelar uma pergunta de inquérito e uma execução de inquérito. O seu inquérito tem três tipos de perguntas, distinguidas pelo formato da resposta: Respostas sim/não, respostas numeradas e respostas de texto. Criar uma public SurveyQuestion
classe:
namespace NullableIntroduction
{
public class SurveyQuestion
{
}
}
O compilador interpreta cada declaração de variável de tipo de referência como um tipo de referência não nulo para código num contexto de anotação anulável ativado. Pode ver o primeiro aviso ao adicionar propriedades para o texto da pergunta e o tipo de pergunta, conforme mostrado no seguinte código:
namespace NullableIntroduction
{
public enum QuestionType
{
YesNo,
Number,
Text
}
public class SurveyQuestion
{
public string QuestionText { get; }
public QuestionType TypeOfQuestion { get; }
}
}
Uma vez que ainda não inicializou QuestionText
o , o compilador emite um aviso a indicar que não foi inicializada uma propriedade não nulo. A estrutura requer que o texto da pergunta não seja nulo, pelo que adiciona um construtor para inicializá-lo e o QuestionType
valor também. A definição de classe concluída é semelhante ao seguinte código:
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);
}
Adicionar o construtor remove o aviso. O argumento construtor também é um tipo de referência não anulável, pelo que o compilador não emite avisos.
Em seguida, crie uma public
classe com o nome SurveyRun
. Esta classe contém uma lista de SurveyQuestion
objetos e métodos para adicionar perguntas ao inquérito, conforme mostrado no seguinte código:
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);
}
}
Tal como anteriormente, tem de inicializar o objeto de lista para um valor não nulo ou o compilador emite um aviso. Não existem verificações nulas na segunda sobrecarga de AddQuestion
porque não são necessárias: declarou que a variável não é nulo. O respetivo valor não pode ser null
.
Mude para Program.cs no seu editor e substitua o conteúdo de Main
pelas seguintes linhas de código:
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?");
Uma vez que todo o projeto está num contexto de anotação anulável ativado, receberá avisos quando passar null
para qualquer método que espere um tipo de referência não anulável. Experimente ao adicionar a seguinte linha a Main
:
surveyRun.AddQuestion(QuestionType.Text, default);
Criar inquiridos e obter respostas para o inquérito
Em seguida, escreva o código que gera respostas para o inquérito. Este processo envolve várias tarefas pequenas:
- Crie um método que gere objetos inquiridos. Estes representam as pessoas que pediram para preencher o inquérito.
- Crie lógica para simular fazer as perguntas a um inquirido e recolher respostas ou notar que um inquirido não respondeu.
- Repita até que os inquiridos tenham respondido ao inquérito.
Precisará de uma classe para representar uma resposta do inquérito, por isso adicione-a agora. Ative o suporte anulável. Adicione uma Id
propriedade e um construtor que a inicialize, conforme mostrado no seguinte código:
namespace NullableIntroduction
{
public class SurveyResponse
{
public int Id { get; }
public SurveyResponse(int id) => Id = id;
}
}
Em seguida, adicione um static
método para criar novos participantes ao gerar um ID aleatório:
private static readonly Random randomGenerator = new Random();
public static SurveyResponse GetRandomId() => new SurveyResponse(randomGenerator.Next());
A principal responsabilidade desta classe é gerar as respostas de um participante às perguntas no inquérito. Esta responsabilidade tem alguns passos:
- Peça a participação no inquérito. Se a pessoa não consentir, devolve uma resposta em falta (ou nula).
- Faça cada pergunta e registe a resposta. Cada resposta também pode estar em falta (ou nula).
Adicione o seguinte código à sua SurveyResponse
classe:
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!";
}
}
O armazenamento das respostas do inquérito é um Dictionary<int, string>?
, que indica que pode ser nulo. Está a utilizar a nova funcionalidade de idioma para declarar a sua intenção de estrutura, tanto para o compilador como para qualquer pessoa que leia o seu código mais tarde. Se alguma vez derreferência surveyResponses
sem verificar primeiro o null
valor, receberá um aviso do compilador. Não recebe um aviso no método porque o AnswerSurvey
compilador pode determinar que a surveyResponses
variável foi definida para um valor não nulo acima.
Utilizar null
para respostas em falta realça um ponto-chave para trabalhar com tipos de referência anuláveis: o seu objetivo não é remover todos os null
valores do programa. Em vez disso, o seu objetivo é garantir que o código que escreve expressa a intenção da sua estrutura. Os valores em falta são um conceito necessário para expressar no seu código. O null
valor é uma forma clara de expressar esses valores em falta. Tentar remover todos os null
valores apenas leva a definir outra forma de expressar esses valores em falta sem null
.
Em seguida, tem de escrever o PerformSurvey
método na SurveyRun
classe . Adicione o seguinte código na SurveyRun
classe :
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);
}
}
Mais uma vez, a sua escolha de um nulo List<SurveyResponse>?
indica que a resposta pode ser nula. Isso indica que o inquérito ainda não foi dado a nenhum inquirido. Repare que os inquiridos são adicionados até que o suficiente tenha consentido.
O último passo para executar o inquérito é adicionar uma chamada para realizar o inquérito no final do Main
método:
surveyRun.PerformSurvey(50);
Examinar as respostas do inquérito
O último passo é apresentar os resultados do inquérito. Irá adicionar código a muitas das classes que escreveu. Este código demonstra o valor de distinguir tipos de referência anuláveis e não anuláveis. Comece por adicionar os seguintes dois membros encorpados de expressão à SurveyResponse
classe:
public bool AnsweredSurvey => surveyResponses != null;
public string Answer(int index) => surveyResponses?.GetValueOrDefault(index) ?? "No answer";
Uma surveyResponses
vez que é um tipo de referência nulo, são necessárias verificações nulas antes de a anular a referência. O Answer
método devolve uma cadeia não anulável, pelo que temos de cobrir as maiúsculas e minúsculas de uma resposta em falta utilizando o operador de agrupamento nulo.
Em seguida, adicione estes três membros encorpados de expressão à SurveyRun
classe :
public IEnumerable<SurveyResponse> AllParticipants => (respondents ?? Enumerable.Empty<SurveyResponse>());
public ICollection<SurveyQuestion> Questions => surveyQuestions;
public SurveyQuestion GetQuestion(int index) => surveyQuestions[index];
O AllParticipants
membro tem de ter em conta que a respondents
variável pode ser nula, mas o valor devolvido não pode ser nulo. Se alterar essa expressão ao remover a ??
sequência vazia e a seguinte, o compilador avisa-o de que o método pode devolver null
e a respetiva assinatura de retorno devolve um tipo não nulo.
Por fim, adicione o seguinte ciclo na parte inferior do Main
método:
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");
}
}
Não precisa de quaisquer null
verificações neste código porque concebeu as interfaces subjacentes para que todas devolvam tipos de referência não anuláveis.
Obter o código
Pode obter o código do tutorial concluído a partir do nosso repositório de exemplos na pasta csharp/NullableIntroduction .
Experimente ao alterar as declarações de tipo entre tipos de referência anuláveis e não anuláveis. Veja como isso gera diferentes avisos para garantir que não derefere acidentalmente um null
.
Passos seguintes
Saiba como utilizar o tipo de referência anulável ao utilizar o Entity Framework: