66 votes

Quelle quantité de vérification de null est suffisante?

Quelles sont les lignes directrices pour déterminer quand il est pas nécessaire de vérifier une valeur nulle ?

Une grande partie du code hérité sur lequel j'ai travaillé récemment comporte des vérifications de null à n'en plus finir. Des vérifications de null sur des fonctions triviales, des vérifications de null sur des appels d'API qui indiquent des retours non nuls, etc. Dans certains cas, les vérifications de null sont raisonnables, mais dans de nombreux cas, une valeur nulle n'est pas une attente raisonnable.

J'ai entendu plusieurs arguments allant de "On ne peut pas se fier aux autres codes" à "TOUJOURS programmer de manière défensive" en passant par "Tant que le langage ne me garantit pas une valeur non nulle, je vais toujours vérifier". Je suis d'accord avec bon nombre de ces principes jusqu'à un certain point, mais j'ai constaté que des vérifications excessives de null causent d'autres problèmes qui vont à l'encontre de ces principes. Est-ce que la vérification acharnée de null en vaut vraiment la peine ?

Fréquemment, j'ai observé que des codes avec des vérifications excessives de null sont en réalité de moindre qualité, pas de qualité supérieure. Une grande partie du code semble tellement centrée sur les vérifications de null que le développeur a perdu de vue d'autres qualités importantes, telles que la lisibilité, la correction, ou la gestion des exceptions. En particulier, je vois beaucoup de code ignorer l'exception std::bad_alloc, mais faire une vérification de null sur un new.

En C++, je comprends cela dans une certaine mesure en raison du comportement imprévisible du déréférencement d'un pointeur nul ; le déréférencement nul est géré de manière plus efficace en Java, C#, Python, etc. Ai-je simplement vu de mauvais exemples de vérification de null vigilante ou y a-t-il vraiment quelque chose à cela ?

Cette question est censée être agnostique par rapport au langage, bien que je m'intéresse principalement au C++, Java et C#.


Quelques exemples de vérifications de null que j'ai vus qui semblent être excessives comprennent les suivants :


Cet exemple semble tenir compte des compilateurs non standards car la spécification C++ indique qu'un new échoué lance une exception. À moins que vous ne souteniez explicitement les compilateurs non conformes, est-ce logique ? Est-ce sensé dans un langage géré comme Java ou C# (ou même C++/CLR) ?

try {
   MyObject* obj = new MyObject(); 
   if(obj!=NULL) {
      //faire quelque chose
   } else {
      //??? la plupart du code que je vois le consigne dans un journal et continue
      //ou il répète ce qui se trouve dans le gestionnaire d'exceptions
   }
} catch(std::bad_alloc) {
   //Faire quelque chose ? généralement -- ce code est erroné car il alloue
   //plus de mémoire et échouera probablement, comme écrire dans un fichier journal.
}

Un autre exemple est lors du travail sur du code interne. Particulièrement, si c'est une petite équipe qui peut définir ses propres pratiques de développement, cela semble inutile. Sur certains projets ou du code hérité, faire confiance à la documentation peut ne pas être raisonnable... mais pour du code récent que vous ou votre équipe contrôlez, est-ce vraiment nécessaire ?

Si une méthode, que vous pouvez voir et modifier (ou réprimander le développeur responsable), a un contrat, est-il toujours nécessaire de vérifier les valeurs nulles ?

//X est non négatif.
//Retourne un objet ou lance une exception.
MyObject* create(int x) {
   if(x<0) throw;
   return new MyObject();
}

try {
   MyObject* x = create(unknownVar);
   if(x!=null) {
      //cette vérification de null est-elle vraiment nécessaire ?
   }
} catch {
   //faire quelque chose
}

Lors du développement d'une fonction privée ou interne, est-il vraiment nécessaire de gérer explicitement une valeur nulle lorsque le contrat demande des valeurs non nulles uniquement ? Pourquoi une vérification de null serait-elle préférable à une assertion ?

(évidemment, dans votre API publique, les vérifications de null sont vitales car il est considéré impoli de gronder vos utilisateurs pour une mauvaise utilisation de l'API)

//Uniquement pour un usage interne--non public, pas partie de l'API publique
//l'entrée ne doit pas être nulle.
//retourne une valeur non négative, ou -1 en cas d'échec
int ParseType(String input) {
   if(input==null) return -1;
   //faire quelque chose de magique
   return valeur;
}

Comparé à :

//Uniquement pour un usage interne--non public, pas partie de l'API publique
//l'entrée ne doit pas être nulle.
//retourne une valeur non négative
int ParseType(String input) {
   assert(input!=null : "L'entrée ne doit pas être nulle.");
   //faire quelque chose de magique
   return valeur;
}

36voto

JoshBerke Points 34238

Une chose à retenir est que le code que vous écrivez aujourd'hui, alors qu'il peut s'agir d'une petite équipe et que vous pouvez avoir une bonne documentation, se transformera en code hérité que quelqu'un d'autre devra maintenir. J'utilise les règles suivantes :

  1. Si j'écris une API publique qui sera exposée à d'autres, alors je ferai des vérifications de null sur tous les paramètres de référence.

  2. Si j'écris un composant interne à mon application, j'écris des vérifications de null lorsque j'ai besoin de faire quelque chose de spécial quand un null existe, ou lorsque je veux le rendre très clair. Sinon, cela ne me dérange pas d'obtenir l'exception de référence nulle car cela est également assez clair ce qui se passe.

  3. Lorsque je travaille avec les données de retour des frameworks d'autres personnes, je ne vérifie que le null lorsqu'il est possible et valide d'avoir un retour null. Si leur contrat dit qu'il ne renvoie pas de null, je ne ferai pas la vérification.

19voto

Steve Jessop Points 166970

Tout d'abord, notez qu'il s'agit d'un cas spécial de vérification de contrat : vous écrivez du code qui ne fait rien d'autre que de valider à l'exécution qu'un contrat documenté est respecté. L'échec signifie qu'il y a un problème quelque part dans le code.

Je suis toujours un peu sceptique à l'idée de mettre en œuvre des cas spéciaux d'un concept plus général. La vérification de contrat est utile car elle permet de détecter les erreurs de programmation dès qu'elles franchissent une limite d'API. Qu'est-ce qui rend les nulls si spéciaux pour qu'ils soient la seule partie du contrat que vous souhaitez vérifier ? Néanmoins,

En ce qui concerne la validation des entrées :

null est spécial en Java : de nombreux APIs Java sont écrits de telle sorte que null est la seule valeur invalide qu'il est même possible de transmettre dans un appel de méthode donné. Dans de tels cas, une vérification de null "valide entièrement" l'entrée, donc l'argument complet en faveur de la vérification de contrat s'applique.

En revanche, en C++, NULL n'est qu'une des près de 2^32 (2^64 sur les nouvelles architectures) valeurs invalides qu'un paramètre de pointeur pourrait prendre, car presque toutes les adresses ne sont pas d'objets du type correct. Vous ne pouvez pas "valider entièrement" votre entrée à moins d'avoir quelque part une liste de tous les objets de ce type.

La question devient alors : NULL est-il une entrée invalide suffisamment courante pour recevoir un traitement spécial que (foo *)(-1) ne reçoit pas ?

Contrairement à Java, les champs ne sont pas auto-initialisés à NULL, donc une valeur non initialisée et inutilisée est tout aussi plausible que NULL. Mais parfois, les objets C++ ont des membres pointeurs qui sont explicitement initialisés à NULL, signifiant "Je n'en ai pas encore". Si votre appelant fait cela, alors il existe une catégorie significative d'erreurs de programmation qui peuvent être diagnostiquées par une vérification de NULL. Une exception peut être plus facile pour eux à déboguer qu'une erreur d'accès mémoire dans une bibliothèque pour laquelle ils n'ont pas le code source. Donc, si vous ne vous souciez pas de l'augmentation du code, cela pourrait être utile. Mais c'est votre appelant auquel vous devez penser, pas à vous-même - ce n'est pas une programmation défensive, car cela ne 'défend' que contre NULL, pas contre (foo *)(-1).

Si NULL n'est pas une entrée valide, vous pourriez envisager de prendre le paramètre par référence plutôt que par pointeur, mais beaucoup de styles de codage désapprouvent les paramètres de référence non-const. Et si l'appelant vous passe *fooptr, où fooptr est NULL, alors cela n'aidera personne de toute façon. Ce que vous essayez de faire, c'est d'ajouter un peu plus de documentation dans la signature de la fonction, dans l'espoir que votre appelant soit plus susceptible de se demander "hmm, est-ce que fooptr pourrait être null ici ?" lorsqu'il doit le déréférencer explicitement, que s'il vous le passe simplement en tant que pointeur. Cela va seulement jusqu'à un certain point, mais dans la mesure où cela va, cela pourrait aider.

Je ne connais pas C#, mais je comprends que c'est comme Java en ce sens que les références ont des valeurs valides garanties (au moins dans le code sécurisé), mais contrairement à Java en ce sens que tous les types n'ont pas de valeur NULL. Je suppose donc que les vérifications de null là-bas ne valent rarement la peine : si vous êtes dans un code sécurisé, n'utilisez pas un type nullable à moins que null soit une entrée valide, et si vous êtes dans un code non sécurisé, la même logique s'applique qu'en C++.

En ce qui concerne la validation de la sortie :

Un problème similaire se pose : en Java, vous pouvez "valider entièrement" la sortie en connaissant son type et en vérifiant que la valeur n'est pas null. En C++, vous ne pouvez pas "valider entièrement" la sortie avec une vérification de NULL - car pour tout ce que vous savez, la fonction a renvoyé un pointeur vers un objet de sa propre pile qui vient d'être déroulée. Mais si NULL est un retour invalide courant en raison des constructions typiquement utilisées par l'auteur du code appelant, alors le vérifier aidera.

Dans tous les cas :

Utilisez les assertions plutôt que du "code réel" pour vérifier les contrats lorsque c'est possible - une fois que votre application fonctionne, vous ne voulez probablement pas de l'augmentation du code où chaque appelant vérifie toutes ses entrées, et chaque appelant vérifie ses valeurs de retour.

Dans le cas de l'écriture de code portable vers des implémentations C++ non standard, alors au lieu du code dans la question qui vérifie NULL et capture également l'exception, j'aurais probablement une fonction comme celle-ci :

template
static inline void nullcheck(T *ptr) { 
    #if PLATFORM_TRAITS_NEW_RETURNS_NULL
        if (ptr == NULL) throw std::bad_alloc();
    #endif
}

Ensuite, parmi la liste des choses que vous faites lors du portage vers un nouveau système, vous définissez PLATFORM_TRAITS_NEW_RETURNS_NULL (et peut-être d'autres PLATFORM_TRAITS) correctement. Évidemment, vous pouvez écrire un en-tête qui le fait pour tous les compilateurs que vous connaissez. Si quelqu'un prend votre code et le compile sur une implémentation C++ non standard dont vous ne savez rien, il est fondamentalement seul pour des raisons plus importantes que celles-ci, donc il devra le faire lui-même.

10voto

MetroidFan2002 Points 11413

Si vous écrivez le code et son contrat, vous êtes responsable de l'utiliser en fonction de son contrat et de vous assurer que le contrat est correct. Si vous dites "retourne un non-null" x, alors l'appelant ne doit pas vérifier le null. Si une exception de pointeur null se produit avec cette référence / pointeur, c'est votre contrat qui est incorrect.

La vérification du nul ne devrait aller à l'extrême que lors de l'utilisation d'une bibliothèque qui n'est pas fiable, ou qui n'a pas de contrat approprié. S'il s'agit du code de votre équipe de développement, insistez sur le fait que les contrats ne doivent pas être violés, et traquez la personne qui utilise incorrectement le contrat lorsque des bugs se produisent.

7voto

djuth Points 700

Une partie de cela dépend de la manière dont le code est utilisé -- s'il s'agit d'une méthode disponible uniquement au sein d'un projet par rapport à une API publique, par exemple. La vérification des erreurs de l'API nécessite quelque chose de plus fort qu'une assertion.

Alors, bien que cela soit parfaitement valable dans un projet où il est pris en charge par des tests unitaires et des choses comme ça :

internal void DoThis(Something thing)
{
    Debug.Assert(thing != null, "L'argument [thing] ne peut pas être null.");
    //...
}

dans une méthode où vous n'avez pas le contrôle sur qui l'appelle, quelque chose comme ceci peut être mieux :

public void DoThis(Something thing)
{
    if (thing == null)
    {
        throw new ArgumentException("L'argument [thing] ne peut pas être null.");
    }
    //...
}

7voto

fizzer Points 8193

Cela dépend de la situation. Le reste de ma réponse suppose que je parle de C++.

  • Je ne teste jamais la valeur de retour de new car toutes les implémentations que j'utilise lancent une bad_alloc en cas d'échec. Si je vois un test hérité pour new renvoyant null dans un code sur lequel je travaille, je le supprime et ne le remplace pas par autre chose.
  • Sauf si des normes de codage étroites l'interdisent, j'affirme les préconditions documentées. Un code cassé violant un contrat publié doit échouer immédiatement et de manière spectaculaire.
  • Si le null résulte d'une défaillance en temps d'exécution qui n'est pas due à un code cassé, je lance une exception. Les échecs de fopen et de malloc (bien que je les utilise rarement, voire jamais en C++) entreraient dans cette catégorie.
  • Je n'essaie pas de récupérer d'une allocation échouée. Bad_alloc est attrapé dans main().
  • Si le test de null concerne un objet qui est collaborateur de ma classe, je réécris le code pour le prendre par référence.
  • Si le collaborateur pourrait vraiment ne pas exister, j'utilise le pattern Null Object pour créer un espace réservé pour échouer de manière bien définie.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X