Esercitazione: Esprimere più chiaramente le finalità di progettazione con tipi riferimento nullable e non nullable

I tipi di riferimento nullable integrano i tipi di riferimento nello stesso modo in cui i tipi di valore nullable completano i tipi di valore di complemento. Per dichiarare una variabile come tipo riferimento nullable basta aggiungere un ? alla fine del tipo. Ad esempio, string? rappresenta un tipo string nullable. È possibile usare questi nuovi tipi per esprimere più chiaramente le finalità della progettazione: alcune variabili devono avere sempre un valore, mentre in altre un valore può mancare.

In questa esercitazione si apprenderà come:

  • Incorporare tipi riferimento nullable e non nullable nelle progettazioni.
  • Abilitare i controlli dei tipi riferimento nullable in tutto il codice.
  • Scrivere codice in cui il compilatore impone tali decisioni di progettazione.
  • Usare la funzionalità dei riferimenti nullable nelle proprie progettazioni.

Prerequisiti

È necessario configurare il computer per eseguire .NET, incluso il compilatore C#. Il compilatore C# è disponibile con Visual Studio 2022 o .NET SDK.

Questa esercitazione presuppone che si abbia familiarità con C# e .NET, inclusi Visual Studio o l'interfaccia della riga di comando .NET.

Incorporare tipi riferimento nullable nelle progettazioni

In questa esercitazione si compilerà una libreria che modella l'esecuzione di un sondaggio. Il codice usa tipi riferimento sia nullable che non nullable per rappresentare concetti reali. Le domande del sondaggio non possono mai essere Null. Un partecipante al sondaggio potrebbe preferire non rispondere a una domanda. Le risposte potrebbero essere null in questo caso.

Il codice scritto per questo esempio esprime questa intenzione e il compilatore la applica.

Creare l'applicazione e abilitare i tipi riferimento nullable

Creare una nuova applicazione console in Visual Studio oppure dalla riga di comando tramite dotnet new console. Assegnare all'applicazione il nome NullableIntroduction. Dopo aver creato l'applicazione, è necessario specificare che l'intero progetto viene compilato in un contesto di annotazione nullable abilitato. Aprire il file con estensione csproj e aggiungere un Nullable elemento all'elemento PropertyGroup . Impostare il relativo valore su enable. È necessario acconsentire esplicitamente alla funzionalità tipi di riferimento nullable nei progetti precedenti a C# 11. Questo perché dopo che la funzionalità viene attivata, le dichiarazioni di variabili di riferimento esistenti diventano tipi riferimento non nullable. Anche se questa decisione aiuterà a trovare problemi in cui il codice esistente potrebbe non avere controlli null appropriati, potrebbe non riflettere in modo accurato la finalità di progettazione originale:

<Nullable>enable</Nullable>

Prima di .NET 6, i nuovi progetti non includono l'elemento Nullable . A partire da .NET 6, i nuovi progetti includono l'elemento <Nullable>enable</Nullable> nel file di progetto.

Progettare i tipi per l'applicazione

Per questa applicazione di sondaggio è necessario creare alcune classi:

  • Una classe che modella l'elenco di domande.
  • Una classe che modella un elenco di persone contattate per il sondaggio.
  • Una classe che modella le risposte di una persona che ha partecipato al sondaggio.

Questi tipi useranno tipi riferimento sia nullable sia non nullable per indicare i membri obbligatori e quelli facoltativi. I tipi riferimento nullable comunicano chiaramente questa finalità della progettazione:

  • Le domande che costituiscono il sondaggio non possono mai essere Null. Una domanda vuota non ha infatti alcun senso.
  • I partecipanti al sondaggio non possono mai essere Null. Si vuole infatti tenere traccia delle persone che sono state contattate, anche quelle che non hanno accettato di partecipare.
  • Una risposta a una domanda può essere Null. I partecipanti possono infatti rifiutarsi di rispondere ad alcune o a tutte le domande.

Se è stato programmato in C#, è possibile che si sia così abituati ai tipi di riferimento che consentono null valori che potrebbero essere state perse altre opportunità per dichiarare istanze non nullable:

  • La raccolta delle domande deve essere non nullable.
  • La raccolta dei partecipanti deve essere non nullable.

Quando si scrive il codice, si noterà che un tipo di riferimento non nullable come impostazione predefinita per i riferimenti evita errori comuni che potrebbero causare NullReferenceExceptions. Una lezione di questa esercitazione è che sono state prese decisioni sulle variabili che potrebbero o non potevano essere null. Nelle versioni precedenti il linguaggio non forniva la sintassi necessaria per esprimere queste decisioni. Ora invece tutto questo è possibile.

L'app che si creerà esegue la procedura seguente:

  1. Crea un sondaggio e aggiunge domande.
  2. Crea un set pseudo-casuale di intervistati per il sondaggio.
  3. Contatta gli intervistati fino al raggiungimento della dimensione del sondaggio completata.
  4. Scrive statistiche importanti sulle risposte al sondaggio.

Compilare il sondaggio con tipi di riferimento nullable e non nullable

Il primo codice scritto crea il sondaggio. Occorre scrivere le classi necessarie per modellare una domanda del sondaggio e l'esecuzione del sondaggio. Il sondaggio ha tre tipi di domande, distinte dal formato della risposta: risposte Sì/No, risposte in forma di numero e risposte testuali. Creare una public SurveyQuestion classe:

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Il compilatore interpreta ogni dichiarazione di variabile di tipo di riferimento come tipo di riferimento non nullable per il codice in un contesto di annotazione nullable abilitato. È possibile vedere il primo avviso aggiungendo le proprietà del testo della domanda e il tipo di domanda, come illustrato nel codice seguente:

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

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

Poiché QuestionText non è stato inizializzato, il compilatore genera un avviso che informa che una proprietà non nullable non è stata inizializzata. Un requisito della progettazione è che il testo della domanda non sia Null, pertanto occorre aggiunge un costruttore per inizializzare questa proprietà e anche il valore QuestionType. La definizione finale della classe è simile al codice seguente:

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

Aggiungendo il costruttore, l'avviso viene rimosso. Anche l'argomento del costruttore è un tipo riferimento non nullable, quindi il compilatore non genera alcun avviso.

Creare quindi una classe public denominata SurveyRun. Questa classe contiene un elenco di metodi e oggetti SurveyQuestion per aggiungere domande al sondaggio, come illustrato nel codice seguente:

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

Anche in questo caso occorre inizializzare l'oggetto elenco su un valore non Null per evitare che il compilatore generi un avviso. Nel secondo overload di AddQuestion non ci sono controlli dei valori Null in quanto non sono necessari: la variabile è stata infatti dichiarata come non nullable. Il suo valore non può essere null.

Passare a Program.cs nell'editor e sostituire il contenuto di con le righe di Main codice seguenti:

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?");

Poiché l'intero progetto si trova in un contesto di annotazione nullable abilitato, si otterranno avvisi quando si passa null a qualsiasi metodo previsto un tipo di riferimento non nullable. Provare ad aggiungere la riga seguente a Main:

surveyRun.AddQuestion(QuestionType.Text, default);

Creare i partecipanti e ottenere le risposte al sondaggio

A questo punto occorre scrivere il codice che genera le risposte al sondaggio. Questo processo include varie piccole attività:

  1. Compilare un metodo che generi gli oggetti partecipante. Questi oggetti rappresentano le persone a cui è stato chiesto di completare il sondaggio.
  2. Creare la logica per simulare la formulazione delle domande a un partecipante e la raccolta delle risposte oppure di nessun dato, se il partecipante non ha risposto.
  3. Ripetere finché non ha risposto al sondaggio un numero sufficiente di partecipanti.

A questo punto occorre aggiungere una classe che rappresenti una risposta al sondaggio. Abilitare il supporto dei tipi riferimento nullable. Aggiungere una proprietà Id e un costruttore che la inizializza, come illustrato nel codice seguente:

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

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

Quindi aggiungere un metodo static per la creazione di nuovi partecipanti tramite la generazione di un ID casuale:

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

La responsabilità principale di questa classe è generare le risposte di un partecipante alle domande del sondaggio. A questo scopo è necessario eseguire i passaggi seguenti:

  1. Chiedere di partecipare al sondaggio. Se la persona non acconsente, restituire una risposta mancante (o Null).
  2. Porre ogni domanda e registrare la risposta. Ogni risposta può anche essere mancante (o Null).

Aggiungere il codice seguente alla classe 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!";
    }
}

L'archivio per le risposte del sondaggio è un Dictionary<int, string>?, a indicare che può essere Null. Si sta usando la nuova funzionalità del linguaggio per dichiarare la finalità della progettazione, sia al compilatore che a chiunque legga il codice successivamente. Se non si verifica mai la dereferenza surveyResponses senza verificare prima il valore, verrà visualizzato un avviso del null compilatore. Non si riceve un avviso nel metodo AnswerSurvey perché il compilatore è in grado di determinare che la variabile surveyResponses è stata impostata su un valore non Null.

L'uso di null per le risposte mancanti evidenzia un aspetto essenziale per l'utilizzo dei tipi riferimento nullable, ovvero l'obiettivo non è rimuovere tutti i valori null dal programma. L'obiettivo è invece quello di assicurarsi che il codice scritto esprima lo scopo della progettazione. I valori mancanti sono un concetto necessario da esprimere nel codice. Il valore null rappresenta un modo chiaro per esprimere tali valori mancanti. Il tentativo di rimuovere tutti i valori null porta solo alla definizione di un altro modo per esprimere i valori mancanti senza null.

A questo punto occorre scrivere il metodo PerformSurvey nella classe SurveyRun. Aggiungere il codice seguente nella classe 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);
    }
}

Anche in questo caso la scelta di un List<SurveyResponse>? nullable indica che la risposta può essere Null. Indica che il sondaggio non è ancora stato distribuito ad alcun partecipante. Si noti che i partecipanti continuano a essere aggiunti finché non acconsente un numero sufficiente.

L'ultimo passaggio dell'esecuzione del sondaggio consiste nell'aggiungere una chiamata per eseguire il sondaggio alla fine del metodo Main:

surveyRun.PerformSurvey(50);

Esaminare le risposte al sondaggio

L'ultimo passaggio è la visualizzazione dei risultati del sondaggio. Occorre aggiungere codice a molte delle classi scritte. Questo codice dimostra il valore della distinzione fra i tipi riferimento nullable e non nullable. Per iniziare, aggiungere i due membri con corpo di espressione seguenti alla classe SurveyResponse:

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

Poiché surveyResponses è un tipo di riferimento nullable, i controlli Null sono necessari prima di de-referencing. Il Answer metodo restituisce una stringa non nullable, quindi è necessario coprire il caso di una risposta mancante usando l'operatore null-coalescing.

Aggiungere quindi questi tre membri con corpo di espressione alla classe SurveyRun:

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

Il membro AllParticipants deve tenere conto del fatto che la variabile respondents potrebbe essere Null, ma il valore restituito non può essere Null. Se si modifica l'espressione rimuovendo ?? e la sequenza vuota che segue, il compilatore avvisa che il metodo potrebbe restituire null e la sua firma restituita restituisce un tipo non nullable.

Infine, aggiungere il ciclo seguente alla fine del metodo 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");
    }
}

Non è necessario eseguire alcun controllo dei valori null in questo codice perché le interfacce sottostanti sono state progettate in modo che restituiscano tutte tipi di riferimento non nullable.

Ottenere il codice

Il codice dell'esercitazione completata è disponibile nel repository samples nella cartella csharp/NullableIntroduction.

È possibile fare delle prove cambiando le dichiarazioni di tipo fra tipi riferimento nullable e non nullable e osservare come vengono generati avvisi diversi per assicurare che non venga dereferenziato accidentalmente un valore null.

Passaggi successivi

Informazioni su come usare il tipo di riferimento nullable quando si usa Entity Framework: