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:

  1. Cria um inquérito e adiciona perguntas ao mesmo.
  2. Cria um conjunto pseudo-aleatório de inquiridos para o inquérito.
  3. Contacta os inquiridos até que o tamanho do inquérito concluído atinja o número do objetivo.
  4. 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 QuestionTexto , 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:

  1. Crie um método que gere objetos inquiridos. Estes representam as pessoas que pediram para preencher o inquérito.
  2. Crie lógica para simular fazer as perguntas a um inquirido e recolher respostas ou notar que um inquirido não respondeu.
  3. 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:

  1. Peça a participação no inquérito. Se a pessoa não consentir, devolve uma resposta em falta (ou nula).
  2. 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: