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

Les types de référence nullables complètent les types de référence de la même façon que les types de valeurs nullables 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 devez configurer votre machine pour exécuter .NET, y compris le compilateur C#. Le compilateur C# est disponible avec Visual Studio 2022 ou le Kit de développement logiciel (SDK) .NET.

Ce didacticiel suppose que vous êtes familiarisé avec C# et .NET, notamment 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, Les réponses peuvent être null dans ce cas.

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 spécifier que l’ensemble du projet est compilé dans un contexte d’annotation nullable activé. Ouvrez le fichier .csproj et ajoutez un Nullable élément à l’élément PropertyGroup . Affectez-lui la valeur enable. Vous devez choisir la fonctionnalité de types de référence nullables 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 vous aide à trouver les problèmes où le code existant peut ne pas avoir de vérifications null appropriées, il se peut qu’elle ne reflète pas exactement votre intention de conception d’origine :

<Nullable>enable</Nullable>

Avant .NET 6, les nouveaux projets n’incluent pas l’élément Nullable . À compter 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 programmé en C#, vous êtes peut-être si habitué aux types de référence qui autorisent null des valeurs que vous avez peut-être manqué d’autres occasions de déclarer des instances non nullables :

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

Au fur et à mesure que vous écrivez le code, vous verrez qu’un type de référence non nullable comme valeur par défaut pour les références évite les erreurs courantes qui pourraient mener à NullReferenceExceptions. L’une des leçons de ce tutoriel est que vous avez pris des décisions concernant les variables qui peuvent ou ne peuvent pas être null. Avant, le langage ne fournissait pas de syntaxe permettant d’exprimer ces décisions ; maintenant, si.

L’application que vous allez générer effectue les étapes suivantes :

  1. Crée une enquête et y ajoute des questions.
  2. Crée un ensemble pseudo-aléatoire de répondants pour l’enquête.
  3. Contacte les répondants jusqu’à ce que la taille de l’enquête terminée atteigne le nombre d’objectifs.
  4. Écrit des statistiques importantes sur les réponses à l’enquête.

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

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 public SurveyQuestion classe :

namespace NullableIntroduction
{
    public class SurveyQuestion
    {
    }
}

Le compilateur interprète chaque déclaration de variable de type référence comme un type de référence 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.

Basculez vers Program.cs dans votre éditeur et remplacez le contenu de Main 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 recevez des avertissements null lorsque vous passez à une méthode qui attend un type de 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 vérifier la null valeur en premier, vous recevrez un avertissement du compilateur. 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";

Étant donné que surveyResponses est un type référence nullable, des vérifications null sont nécessaires avant de le déréférencer. La Answer méthode retourne une chaîne non nullable. Nous devons donc couvrir le cas d’une réponse manquante à l’aide de l’opérateur de fusion null.

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 utiliser le type référence nullable lors de l’utilisation d’Entity Framework :