web-dev-qa-db-fra.com

Doit-on vérifier null s'il n'attend pas null?

La semaine dernière, nous avons eu un débat passionné sur la gestion des null dans la couche de service de notre application. La question est dans le contexte .NET, mais ce sera la même dans Java et de nombreuses autres technologies.

La question était la suivante: devez-vous toujours vérifier les valeurs nulles et faire fonctionner votre code quoi qu'il arrive, ou laisser une exception se propager lorsqu'une valeur nulle est reçue de manière inattendue?

D'un côté, vérifier null où vous ne vous y attendez pas (c'est-à-dire ne pas avoir d'interface utilisateur pour le gérer) est, à mon avis, la même chose que d'écrire un bloc try avec un catch vide. Vous cachez simplement une erreur. L'erreur peut être que quelque chose a changé dans le code et null est maintenant une valeur attendue, ou il y a une autre erreur et le mauvais ID est passé à la méthode.

D'un autre côté, la vérification des valeurs nulles peut être une bonne habitude en général. De plus, s'il y a un contrôle, l'application peut continuer à fonctionner, avec juste une petite partie de la fonctionnalité sans effet. Ensuite, le client peut signaler un petit bogue comme "ne peut pas supprimer le commentaire" au lieu d'un bogue beaucoup plus grave comme "ne peut pas ouvrir la page X".

Quelle pratique suivez-vous et quels sont vos arguments pour ou contre l'une ou l'autre approche?

Mise à jour:

Je veux ajouter quelques détails sur notre cas particulier. Nous récupérions certains objets de la base de données et avons effectué un traitement sur eux (disons, construisons une collection). Le développeur qui a écrit le code ne s'attendait pas à ce que l'objet puisse être nul, il n'a donc inclus aucune vérification, et lorsque la page a été chargée, il y a eu une erreur et la page entière ne s'est pas chargée.

De toute évidence, dans ce cas, il aurait dû y avoir un chèque. Ensuite, nous sommes entrés dans un argument pour savoir si chaque objet traité doit être vérifié, même s'il ne devrait pas manquer, et si le traitement éventuel doit être abandonné en silence.

L'avantage hypothétique serait que la page continuera à fonctionner. Pensez à des résultats de recherche sur Stack Exchange dans différents groupes (utilisateurs, commentaires, questions). La méthode pourrait vérifier la nullité et abandonner le traitement des utilisateurs (qui, en raison d'un bogue, est nul), mais renvoyer les sections "commentaires" et "questions". La page continuerait à fonctionner sauf que la section "utilisateurs" sera manquante (ce qui est un bug). Faut-il échouer tôt et casser la page entière ou continuer à travailler et attendre que quelqu'un remarque que la section "utilisateurs" est manquante?

119
Stilgar

La question n'est pas tant de savoir si vous devez vérifier null ou laisser le runtime lever une exception; c'est ainsi que vous devez réagir à une situation aussi inattendue.

Vos options sont donc les suivantes:

  • Lancez une exception générique (NullReferenceException) et laissez-la bouillonner; si vous ne faites pas vous-même la vérification de null, c'est ce qui se produit automatiquement.
  • Lancez une exception personnalisée qui décrit le problème à un niveau supérieur; ceci peut être réalisé soit en lançant la vérification null, soit en interceptant un NullReferenceException et en lançant l'exception plus spécifique.
  • Récupérez en substituant une valeur par défaut appropriée.

Il n'y a pas de règle générale qui est la meilleure solution. Personnellement, je dirais:

  • L'exception générique est préférable si la condition d'erreur est le signe d'un bogue grave dans votre code, c'est-à-dire que la valeur nulle ne doit jamais être autorisée à y arriver en premier lieu. Une telle exception peut alors remonter jusqu'à la journalisation des erreurs que vous avez configurée, afin que quelqu'un soit averti et corrige le bogue.
  • La solution de valeur par défaut est bonne si la fourniture d'une sortie semi-utile est plus importante que l'exactitude, comme un navigateur Web qui accepte du HTML techniquement incorrect et s'efforce de le rendre de manière sensée.
  • Sinon, j'irais avec l'exception spécifique et la gérerais dans un endroit approprié.
109
tdammers

À mon humble avis, essayer de gérer des valeurs nulles auxquelles vous ne vous attendez pas conduit à un code trop compliqué. Si vous ne vous attendez pas à ce que ce soit nul, dites-le clairement en lançant ArgumentNullException. Je suis vraiment frustré lorsque les gens vérifient si la valeur est nulle, puis essaient d'écrire du code qui n'a aucun sens. La même chose s'applique à l'utilisation de SingleOrDefault (ou pire encore à obtenir une collection) lorsque quelqu'un attend vraiment Single et bien d'autres cas où les gens ont peur (je ne sais pas vraiment quoi) d'exprimer leur logique clairement.

60
empi

L'habitude de vérifier null d'après mon expérience vient d'anciens développeurs C ou C++, dans ces langages, vous avez de bonnes chances de cacher une erreur grave lorsque pas vérification de NULL. Comme vous le savez, en Java ou C # les choses sont différentes, utiliser une référence nulle sans vérifier null provoquera une exception, ainsi l'erreur ne sera pas cachée secrètement tant que vous ne l'ignorez pas du côté appelant. Donc, dans la plupart des cas, la vérification explicite de null n'a pas beaucoup de sens et complique le code plus que nécessaire. C'est pourquoi je ne pas = considérer la vérification nulle comme une bonne habitude dans ces langues (du moins pas en général).

Bien sûr, il y a des exceptions à cette règle, voici celles auxquelles je peux penser:

  • vous voulez un meilleur message d'erreur lisible par l'homme, indiquant à l'utilisateur dans quelle partie du programme la mauvaise référence nulle s'est produite. Ainsi, votre test pour null lève juste une exception différente, avec un texte d'erreur différent. De toute évidence, aucune erreur ne sera masquée de cette façon.

  • vous voulez que votre programme "échoue plus tôt". Par exemple, vous souhaitez vérifier la valeur null d'un paramètre constructeur, où la référence d'objet autrement ne serait stockée que dans une variable membre et utilisée ultérieurement.

  • vous pouvez gérer la situation d'une référence nulle en toute sécurité, sans risque d'erreurs ultérieures, et sans masquer une erreur grave.

  • vous souhaitez un type de signalisation d'erreur complètement différent (par exemple, renvoyer un code d'erreur) dans votre contexte actuel

37
Doc Brown

Utilisez affirme pour tester et indiquer les pré/postconditions et les invariants de votre code.

Cela rend beaucoup plus facile de comprendre ce que le code attend et ce qu'il peut s'attendre à gérer.

Une assertion, l'OMI, est une vérification appropriée car elle est:

  • efficace (généralement pas utilisé dans la version),
  • bref (généralement une seule ligne) et
  • clair (condition préalable non respectée, il s'agit d'une erreur de programmation).

Je pense que le code d'auto-documentation est important, donc avoir un chèque est bon. La programmation défensive, rapide, facilite la maintenance, ce qui est généralement le temps passé.

Mise à jour

Wrt votre élaboration. Il est généralement bon de donner à l'utilisateur final une application qui fonctionne bien sous les bogues, c'est-à-dire qui montre autant que possible. La robustesse est bonne, car le ciel ne sait que ce qui se cassera à un moment critique.

Cependant, les développeurs et les testeurs doivent être conscients des bogues dès que possible, donc vous voulez probablement un cadre de journalisation avec un crochet qui peut afficher une alerte à l'utilisateur en cas de problème. L'alerte doit probablement être affichée à tous les utilisateurs, mais peut probablement être adaptée différemment selon l'environnement d'exécution.

15
Macke

Échouez tôt, échouez souvent. null est l'une des meilleures valeurs inattendues que vous puissiez obtenir, car vous pouvez échouer rapidement dès que vous essayez de l'utiliser. D'autres valeurs inattendues ne sont pas si faciles à détecter. Puisque null échoue automatiquement pour vous chaque fois que vous essayez de l'utiliser, je dirais qu'il ne nécessite pas de vérification explicite.

8
DeadMG
  • Vérifier une valeur nulle devrait être votre seconde nature

  • Votre API sera utilisée par d'autres personnes et leurs attentes seront probablement différentes des vôtres

  • Des tests unitaires approfondis mettent normalement en évidence la nécessité d'une vérification de référence nulle

  • La vérification des valeurs nulles n'implique pas des instructions conditionnelles. Si vous craignez que la vérification null rend votre code moins lisible, vous pouvez envisager des contrats de code .NET.

  • Pensez à installer ReSharper (ou similaire) pour rechercher dans votre code les vérifications de références nulles manquantes

6
CodeART

Je considérerais la question du point de vue de la sémantique, c'est-à-dire me demander ce que représente un pointeur NULL ou un argument de référence nul.

Si une fonction ou une méthode foo () a un argument x de type T, x peut être obligatoire ou facultatif.

En C++, vous pouvez passer un argument obligatoire comme

  • Une valeur, par ex. void foo (T x)
  • Une référence, par ex. void foo (T & x).

Dans les deux cas, vous n'avez pas le problème de vérifier un pointeur NULL. Ainsi, dans de nombreux cas, vous n'avez pas besoin d'une vérification de pointeur nul pas du tout pour les arguments obligatoires.

Si vous passez un argument facultatif, vous pouvez utiliser un pointeur et utiliser la valeur NULL pour indiquer qu'aucune valeur n'a été donnée:

void foo(T* x)

Une alternative consiste à utiliser un pointeur intelligent comme

void foo(shared_ptr<T> x)

Dans ce cas, vous souhaiterez probablement vérifier le pointeur dans le code, car cela fait une différence pour la méthode foo () si x contient une valeur ou aucune valeur du tout.

Le troisième et dernier cas est que vous utilisez un pointeur pour un argument obligatoire. Dans ce cas, appeler foo (x) avec x == NULL est un erreur et vous devez décider comment gérer cela (la même chose qu'avec un index hors limites ou toute entrée non valide) :

  1. Pas de vérification: s'il y a un bug, laissez-le planter et espérons que ce bug apparaîtra assez tôt (pendant les tests). Si ce n'est pas le cas (si vous échouez suffisamment tôt), espérons qu'il n'apparaîtra pas dans un système de production, car l'expérience utilisateur sera de voir l'application se bloquer.
  2. Vérifiez tous les arguments au début de la méthode et signalez les arguments NULL invalides, par ex. lève une exception de foo () et laisse une autre méthode la gérer. La meilleure chose à faire est probablement d'afficher à l'utilisateur une fenêtre de dialogue indiquant que l'application a rencontré une erreur interne et va être fermée.

Mis à part une meilleure façon d'échouer (donner plus d'informations à l'utilisateur), un avantage de la deuxième approche est qu'il est plus probable que le bogue soit trouvé: le programme échouera chaque fois que la méthode est appelée avec les mauvais arguments alors qu'avec approche 1, le bogue n'apparaîtra que si une instruction déréférençant le pointeur est exécutée (par exemple si la branche droite d'une instruction if est entrée). L'approche 2 offre donc une forme plus forte de "échec précoce".

Dans Java vous avez moins de choix car tous les objets sont passés en utilisant des références qui peuvent toujours être null. Encore une fois, si le null représente une valeur NONE d'un argument facultatif, alors vérifier le null sera (probablement) faire partie de la logique de mise en œuvre.

Si la valeur null est une entrée non valide, vous avez une erreur et j'appliquerais des considérations similaires à celles des pointeurs C++. Puisque dans Java vous pouvez intercepter les exceptions de pointeur nul, l'approche 1 ci-dessus est équivalente à l'approche 2 si la méthode foo () déréférence tous les arguments d'entrée dans tous les chemins d'exécution (ce qui se produit probablement très souvent pour les petites méthodes) .

Résumé

  • En C++ et en Java, vérifier si arguments facultatifs sont nuls fait partie de la logique du programme.
  • En C++, je vérifierais toujours arguments obligatoires pour m'assurer qu'une bonne exception est levée afin que le programme puisse la gérer de manière robuste.
  • Dans Java (ou C #), je vérifierais arguments obligatoires s'il y a des chemins d'exécution qui ne seront pas lancés afin d'avoir une forme plus forte de "fail early" .

MODIFIER

Merci à Stilgar pour plus de détails.

Dans votre cas, il semble que vous ayez null comme résultat d'une méthode. Encore une fois, je pense que vous devez d'abord clarifier (et corriger) la sémantique de vos méthodes avant de pouvoir prendre une décision.

Ainsi, vous avez la méthode m1 () appelant la méthode m2 () et m2 () renvoie une référence (en Java) ou un pointeur (en C++) vers un objet.

Quelle est la sémantique de m2 ()? M2 () doit-il toujours renvoyer un résultat non nul? Si tel est le cas, un résultat nul est un --- error interne (en m2 ()). Si vous vérifiez la valeur de retour dans m1 (), vous pouvez avoir une gestion des erreurs plus robuste. Si vous ne le faites pas, vous aurez probablement une exception, tôt ou tard. Peut être pas. J'espère que l'exception est levée pendant les tests et non après le déploiement. Question: si m2 () ne doit jamais retourner null, pourquoi retourne-t-il null? Peut-être devrait-il lever une exception à la place? m2 () est probablement buggé.

L'alternative est que le retour de null fait partie de la sémantique de la méthode m2 (), c'est-à-dire qu'il a un résultat facultatif, qui devrait être documenté dans la documentation Javadoc de la méthode. Dans ce cas, il est OK d'avoir une valeur nulle. La méthode m1 () doit la vérifier comme n'importe quelle autre valeur: il n'y a pas d'erreur ici mais vérifier si le résultat est nul ne fait que partie de la logique du programme.

4
Giorgio

Mon approche générale est de ne jamais tester une condition d'erreur que vous ne savez pas comment gérer . Ensuite, la question à laquelle il faut répondre dans chaque instance spécifique devient: pouvez-vous faire quelque chose de raisonnable en présence de la valeur inattendue de null?

Si vous pouvez continuer en toute sécurité en substituant une autre valeur (une collection vide, par exemple, ou une valeur ou une instance par défaut), faites-le par tous les moyens. @ l'exemple de Shahbaz de World of Warcraft de remplacer une image par une autre tombe dans cette catégorie; l'image elle-même n'est probablement qu'une décoration et n'a pas d'impact fonctionnel. (Dans une version de débogage, j'utiliserais une couleur hurlante pour attirer l'attention sur le fait qu'une substitution s'est produite, mais cela dépend beaucoup de la nature de l'application.) Consignez les détails de l'erreur, cependant, afin que vous sachiez qu'elle se produit; sinon, vous masquerez une erreur que vous souhaitez probablement connaître. Le cacher à l'utilisateur est une chose (encore une fois, en supposant que le traitement peut se poursuivre en toute sécurité); le cacher au développeur en est une autre.

Si vous ne pouvez pas continuer en toute sécurité en présence d'une valeur nulle, échouez avec une erreur sensible; en général, je préfère échouer dès que possible lorsqu'il est évident que l'opération ne peut pas réussir, ce qui implique de vérifier les conditions préalables. Il y a des moments où vous ne voulez pas échouer immédiatement, mais retardez l'échec aussi longtemps que possible; cette considération revient par exemple dans les applications liées à la cryptographie, où les attaques par canal latéral pourraient autrement divulguer des informations que vous ne souhaitez pas connaître. À la fin, laissez la bulle d'erreur jusqu'à un gestionnaire d'erreur général, qui enregistre à son tour les détails pertinents et affiche un message d'erreur Nice (aussi agréable soit-il) à l'utilisateur.

Il convient également de noter que la réponse appropriée peut très bien différer entre les versions de test/QA/débogage et les versions de production/version. Dans les versions de débogage, j'irais pour échouer tôt et dur avec des messages d'erreur très détaillés, pour rendre complètement évident qu'il y a un problème et permettre à un post-mortem de déterminer ce qui doit être fait pour le résoudre. Les builds de production, face à des erreurs récupérables en toute sécurité, devraient probablement favoriser la récupération.

4
a CVn

Comme l'ont indiqué les autres réponses Si vous ne vous attendez pas à NULL, expliquez-le en lançant ArgumentNullException. À mon avis, lorsque vous déboguez le projet cela vous aide à découvrir les défauts dans la logique de votre programme plus tôt.

Donc, vous allez publier votre logiciel, si vous restaurez ces vérifications NullRefrences, vous ne manquez rien sur la logique de votre logiciel, cela vous permet de vous assurer qu'un bogue grave n'apparaîtra pas.

Un autre point est que si la vérification de NULL porte sur les paramètres d'une fonction, alors vous pouvez décider si la fonction est le bon endroit pour le faire ou non; Sinon, recherchez toutes les références à la fonction, puis avant de passer un paramètre à la fonction, passez en revue les scénarios probables qui peuvent conduire à une référence nulle.

2
Ahmad

Bien sûr, NULL n'est pas attendu , mais il y a toujours des bugs!

Je crois que vous devriez vérifier NULL si vous ne vous y attendez pas. Permettez-moi de vous donner des exemples (bien qu'ils ne soient pas en Java).

  • Dans World of Warcraft, en raison d'un bug, l'image d'un des développeurs est apparue sur un cube pour de nombreux objets. Imaginez que si le moteur graphique n'attendait pas NULL pointeurs, il y aurait eu un plantage, alors qu'il était devenu non expérience fatale (voire drôle) pour l'utilisateur. Le moins est que vous n'auriez pas perdu de façon inattendue votre connexion ou l'un de vos points de sauvegarde.

    Il s'agit d'un exemple où la vérification de NULL a empêché un crash et le cas a été traité en silence.

  • Imaginez un programme de guichetier bancaire. L'interface effectue quelques vérifications et envoie des données d'entrée à une partie sous-jacente pour effectuer les transferts d'argent. Si la partie centrale s'attend à ce que ne reçoive pas NULL, alors un bogue dans l'interface peut provoquer un problème dans la transaction, éventuellement de l'argent au milieu se perdre (ou pire, en double).

    Par "un problème", j'entends un crash (comme dans crash crash (disons que le programme est écrit en C++), pas une exception de pointeur nul). Dans ce cas, la transaction doit être annulée si NULL est rencontré (ce qui bien sûr ne serait pas possible si vous n'avez pas vérifié les variables par rapport à NULL!)

Je suis sûr que vous avez compris l'idée.

La vérification de la validité des arguments de vos fonctions n'encombre pas votre code, car il s'agit de quelques vérifications en haut de votre fonction (non réparties au milieu) et augmente considérablement la robustesse.

Si vous devez choisir entre "le programme indique à l'utilisateur qu'il a échoué et s'arrête avec élégance ou revient à un état cohérent, créant éventuellement un journal des erreurs" et "Violation d'accès", ne choisiriez-vous pas le premier? Cela signifie que chaque fonction doit être préparée pour être appelée par un code de bogue plutôt que en attendant que tout soit correct, simplement parce que les bogues existent toujours.

2
Shahbaz

Chaque null de votre système est une bombe à retardement qui attend d'être découverte. Recherchez null aux limites où votre code interagit avec le code que vous ne contrôlez pas et interdisez null dans votre propre code. Cela vous débarrasse du problème sans faire naïvement confiance au code étranger. Utilisez des exceptions ou une sorte de type Maybe/Nothing à la place.

1
Doval

Bien qu'une exception de pointeur nul soit finalement levée, elle sera souvent jetée loin du point où vous avez obtenu le pointeur nul. Rendez-vous service et raccourcissez le temps nécessaire pour corriger le bogue en enregistrant au moins le fait que vous avez créé un pointeur nul dès que possible.

Même si vous ne savez pas quoi faire maintenant, l'enregistrement de l'événement vous fera gagner du temps plus tard. Et peut-être que le gars qui obtient le ticket pourra alors utiliser le temps gagné pour trouver la bonne réponse.

0
Jon Strayer