Tutoriel : Exprimer plus clairement une intention de conception avec les types référence Nullable et non Nullable

Les types référence nullable, qui viennent compléter les types référence de la même façon que les types valeur nullable complètent les types valeur. Pour déclarer une variable comme étant un type référence Nullable, on ajoute ? au type. Par exemple, string? représente une string Nullable. Vous pouvez utiliser ces nouveaux types pour exprimer plus clairement votre intention de conception : certaines variables doivent toujours avoir une valeur, d’autres peuvent ne pas en avoir.

Ce didacticiel vous montre comment effectuer les opérations suivantes :

  • incorporer des types référence Nullable et non Nullable dans vos conceptions ;
  • activer les contrôles de type référence Nullable dans l’ensemble de votre code ;
  • écrire du code permettant au compilateur d’appliquer ces décisions de conception ;
  • utiliser la fonctionnalité de référence Nullable dans vos propres conceptions.

Prérequis

Vous devrez configurer votre ordinateur de façon à exécuter .NET, avec le compilateur C#. Le compilateur C# est accessible via Visual Studio 2022 ou le SDK .NET.

Ce tutoriel suppose de connaître C# et .NET, y compris Visual Studio ou l’interface CLI .NET.

Incorporer des types référence Nullable dans des conceptions

Dans ce tutoriel, vous allez générer une bibliothèque qui modélise la réalisation d’une enquête. Le code utilise des types référence Nullable et non Nullable pour représenter les concepts du monde réel. Les questions de l’enquête ne peuvent jamais être Null. Si la personne interrogée ne souhaite pas répondre à une question, la réponse peut être null.

Le code que vous allez écrire pour cet exemple exprime cette intention, que le compilateur se charge d’appliquer.

Créer l’application et activer les types référence Nullable

Créez une application console dans Visual Studio ou en ligne de commande avec dotnet new console. Nommez l'application NullableIntroduction. Une fois que vous avez créé l’application, vous devez préciser que l’ensemble du projet est compilé dans un contexte d’annotation nullable activé. Ouvrez le fichier .csproj et ajoutez un élément Nullable à l’élément PropertyGroup. Affectez-lui la valeur enable. Vous devez activer la fonctionnalité types référence nullable dans les projets antérieurs à C# 11. En effet, une fois la fonctionnalité activée, les déclarations de variables référence existantes deviennent des types référence non Nullable. Bien que cette décision aide à identifier les problèmes de contrôle de type Null dans le code, elle ne reflète pas forcément l’intention de conception d’origine :

<Nullable>enable</Nullable>

Avant .NET 6, les nouveaux projets n’incluent pas l’élément Nullable. À partir de .NET 6, les nouveaux projets incluent l’élément <Nullable>enable</Nullable> dans le fichier projet.

Concevoir les types de l’application

Cette application d’enquête implique la création d’un certain nombre de classes :

  • une classe qui modélise la liste des questions ;
  • une classe qui modélise la liste des personnes contactées pour l’enquête ;
  • une classe qui modélise les réponses d’une personne ayant répondu à l’enquête.

Ces types utilisent des types référence Nullable et non Nullable pour indiquer quels membres sont requis et lesquels sont facultatifs. Les types référence Nullable communiquent clairement cette intention de conception :

  • Les questions qui font partie de l’enquête ne peuvent jamais être Null : poser une question vide n’aurait pas de sens.
  • Les personnes interrogées ne peuvent jamais être Null. Vous souhaitez faire un suivi des personnes que vous avez contactées, même de celles qui ont refusé de participer.
  • La réponse à une question peut être Null. Les personnes interrogées peuvent refuser de répondre à certaines questions ou à la totalité d’entre elles.

Si vous avez déjà programmé en C#, vous avez peut-être une telle habitude des types référence qui autorisent les valeurs null que vous avez raté certaines occasions de déclarer des instances non nullable :

  • La collection de questions doit être non Nullable.
  • La collection de personnes interrogées doit être non Nullable.

En écrivant le code, vous allez voir qu’utiliser par défaut un type référence non Nullable pour les références permet d’éviter des erreurs courantes susceptibles d’aboutir à des exceptions NullReferenceException. Ce tutoriel vous aide à prendre des décisions sur les variables qui peuvent ou non être null. Avant, le langage ne fournissait pas de syntaxe permettant d’exprimer ces décisions ; maintenant, si.

Voici ce que l’application que vous allez créer va faire :

  1. Créer une enquête et y ajouter des questions.
  2. Créer un ensemble pseudo-aléatoire de répondants pour l’enquête.
  3. Contacter les répondants jusqu’à ce que l’enquête atteigne la taille visée.
  4. Écrire des statistiques importantes sur les réponses à l’enquête.

Créer l’enquête avec des types référence nullable et non nullable

La première partie du code crée l’enquête. Vous allez écrire des classes pour modéliser une question et une enquête. L’enquête comporte trois types de questions, en fonction du format de la réponse : réponses par oui ou non, réponses chiffrées et réponses textuelles. Créez une classe public SurveyQuestion :

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Le compilateur interprète chaque déclaration de variable de type référence comme non nullable pour le code dans un contexte d’annotation nullable activé. Un premier avertissement apparaît lorsque l’on ajoute des propriétés pour le texte et le type de la question avec le code suivant :

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

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

Comme QuestionText n’est pas initialisé, le compilateur émet l’avertissement selon lequel une propriété non Nullable n’a pas été initialisée. Or, la conception exige que le texte de la question soit non Null. Ajoutez un constructeur pour l’initialiser, ainsi que la valeur QuestionType. Une fois terminée, la définition de classe se présente ainsi :

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

Le fait d’ajouter le constructeur supprime l’avertissement. L’argument du constructeur étant également un type référence non Nullable, le compilateur ne génère pas d’avertissements.

Ensuite, créez une classe public nommée SurveyRun. Cette classe contient une liste d’objets SurveyQuestion et de méthodes permettant d’ajouter des questions à l’enquête, comme l’illustre le code suivant :

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

Comme tout à l’heure, il faut initialiser l’objet de liste sur une valeur non Null pour éviter que le compilateur n’émette un avertissement. Il n’y a pas de contrôle de type Null dans la deuxième surcharge de AddQuestion, car ce n’est pas nécessaire : vous avez déclaré cette variable comme étant non Nullable. Sa valeur ne peut pas être null.

Passez à Program.csMain dans votre éditeur et remplacez le contenu de par les lignes de code suivantes :

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

Étant donné que l’ensemble du projet se trouve dans un contexte d’annotation nullable activé, vous obtenez des avertissements au moment où vous transmettez null à une méthode qui attend un type référence non nullable. Regardez le résultat en ajoutant la ligne suivante à Main :

surveyRun.AddQuestion(QuestionType.Text, default);

Créer des personnes interrogées et obtenir des réponses à l’enquête

Ensuite, écrivez le code qui génère des réponses à l’enquête. Ce processus implique plusieurs petites tâches :

  1. Créer une méthode qui génère des objets personne interrogée, représentant les personnes à qui il a été demandé de répondre à l’enquête.
  2. Créer une logique pour simuler les questions posées à une personne interrogée et les réponses obtenues, ou noter qu’une personne interrogée n’a pas répondu.
  3. Répéter le processus jusqu'à ce que les personnes interrogées soient en nombre suffisant.

Il vous faut une classe qui représente les réponse à l’enquête. Ajoutez-la maintenant. Activez la prise en charge Nullable. Ajoutez une propriété Id et un constructeur qui l’initialise, comme dans le code suivant :

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

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

Ensuite, ajoutez une méthode static pour créer de nouveaux participants en générant un ID aléatoire :

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

La responsabilité principale de cette classe consiste à générer les réponses d’un participant aux questions de l’enquête, ce qui se décompose en plusieurs étapes :

  1. Demander à participer à l’enquête. Si la personne refuse, renvoyer une réponse manquante (Null).
  2. Poser chaque question et enregistrer la réponse. Chacune des réponses peut également être manquante (Null).

Ajoutez le code suivant à la 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!";
    }
}

Les réponses à l’enquête sont stockées dans un Dictionary<int, string>?, qui peut donc être Null. Vous utilisez la nouvelle fonctionnalité du langage pour déclarer votre intention de conception, à la fois au compilateur et à toute personne qui lira votre code. Si vous déréférencez surveyResponses sans d’abord contrôler la valeur null, le compilateur émet un avertissement. La méthode AnswerSurvey ne génère pas d’avertissement, car le compilateur peut déterminer que la variable surveyResponses a été définie avant sur une valeur non Null.

L’utilisation de null pour les réponses manquantes met en évidence un point important pour travailler avec les types référence nullable : votre objectif n’est pas de supprimer toutes les valeurs null à partir de votre programme. Au lieu de cela, votre objectif est de vous assurer que le code que vous écrivez exprime l’intention de votre conception. Les valeurs manquantes sont un concept nécessaire à exprimer dans votre code. La valeur null est une méthode évidente pour exprimer les valeurs manquantes. Essayer de supprimer toutes les valeurs null mène uniquement à la définition d’une autre façon d’exprimer ces valeurs manquantes sans null.

Ensuite, il reste à écrire la méthode PerformSurvey dans la classe SurveyRun. Ajoutez le code suivant à la 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);
    }
}

Là encore, le choix d’une List<SurveyResponse>? Nullable indique que la réponse peut être Null, et que l’enquête n’a pour le moment été menée auprès de personne. Des personnes interrogées sont ajoutées jusqu'à ce qu’elles soient suffisamment nombreuses à avoir accepté.

La dernière étape consiste à ajouter un appel pour effectuer l’enquête à la fin de la méthode Main :

surveyRun.PerformSurvey(50);

Examiner les réponses à l’enquête

La dernière étape consiste à afficher les résultats de l’enquête, ce qui suppose d’ajouter du code à la plupart des classes déjà écrites. Ce code montre l’intérêt de distinguer les types référence Nullable et non Nullable. Commencez par ajouter les deux membres expression-bodied suivants à la classe SurveyResponse :

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

surveyResponses étant un type référence non nullable, son déréférencement nécessite au préalable une recherche de valeurs null. Comme la méthode Answer retourne une chaîne non nullable, nous devons prévoir le cas où une réponse est manquante en utilisant l’opérateur de coalescence nulle.

Ensuite, ajoutez ces trois membres expression-bodied à la classe SurveyRun :

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

Le membre AllParticipants doit prendre en compte le fait que la variable respondents peut être Null, mais pas la valeur de retour. Si vous modifiez cette expression en supprimant ?? et la séquence vide qui suit, le compilateur émet un avertissement, indiquant que la méthode peut retourner null et sa signature de retour un type non Nullable.

Enfin, ajoutez la boucle suivante en bas de la méthode 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");
    }
}

Aucun contrôle null n’est nécessaire dans ce code, car vous avez conçu les interfaces sous-jacentes de sorte qu’elles retournent toutes des types référence non Nullable.

Obtenir le code

Pour obtenir le code du tutoriel complet, consultez notre dépôt samples dans le dossier csharp/NullableIntroduction.

Faites des essais en modifiant les déclarations de type entre les types référence Nullable et non Nullable. Examinez les différents avertissements générés, qui visent à empêcher de déréférencer accidentellement un null.

Étapes suivantes

Découvrez comment employer le type référence nullable quand Entity Framework est utilisé :