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?
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:
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.null
, soit en interceptant un NullReferenceException
et en lançant l'exception plus spécifique.Il n'y a pas de règle générale qui est la meilleure solution. Personnellement, je dirais:
À 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.
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
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:
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.
É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.
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
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
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) :
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é
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.
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.
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.
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.
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.
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.