Pourquoi les références nulles sont-elles évitées alors que lever des exceptions est considéré comme correct?


21

Je ne comprends pas très bien le dénigrement cohérent des références nulles par certains utilisateurs du langage de programmation. Qu'est-ce qui est si mauvais avec eux? Si je demande un accès en lecture à un fichier qui n'existe pas, je suis parfaitement heureux d'obtenir une exception ou une référence nulle et pourtant les exceptions sont considérées comme bonnes mais les références nulles sont considérées comme mauvaises. Quel est le raisonnement derrière cela?



2
Certaines langues se bloquent mieux que d'autres sur null. Pour un "code managé", tel que .Net / Java, une référence nulle est juste un autre type de problème, tandis que d'autres codes natifs peuvent ne pas gérer cela avec élégance (vous n'avez pas mentionné de langue spécifique). Même dans un monde géré, parfois vous voulez écrire du code à sécurité intégrée (intégré?, Armes?), Et parfois vous voulez chier fort ASAP (test unitaire). Les deux types de code pourraient appeler dans la même bibliothèque - ce serait un problème. En général, je pense que ce code qui essaie de ne pas nuire aux sentiments des ordinateurs est une mauvaise idée. De toute façon, la sécurité contre les pannes est DURE.
Job

@Job: Cela prône la paresse. Si vous savez comment gérer une exception, vous la gérez. Parfois, cette gestion peut impliquer le lancement d'une autre exception, mais vous ne devez jamais laisser une exception de référence nulle non gérée. Déjà. C'est le cauchemar de tout programmeur de maintenance; c'est l'exception la plus inutile de tout l'arbre. Demandez simplement à Stack Overflow .
Aaronaught

ou pour le dire autrement - pourquoi ne pas retourner un code quelconque pour représenter les informations d'erreur. Cet argument fera rage pour les années à venir.
gbjbaanb

Réponses:


24

Les références nulles ne sont pas "évitées" plus que les exceptions, du moins par quiconque que j'ai jamais connu ou lu. Je pense que vous comprenez mal la sagesse conventionnelle.

Ce qui est mauvais, c'est une tentative d' accéder à une référence nulle (ou de déréférencer un pointeur nul, etc.). C'est mauvais car cela indique toujours un bug; vous ne feriez jamais quelque chose comme ça sur le but, et si vous êtes en train de faire exprès, alors que est encore pire, parce que cela rend impossible de distinguer le comportement attendu d' un comportement buggy.

Il y a certains groupes marginaux qui détestent vraiment le concept de nullité pour une raison quelconque, mais comme Ed le fait remarquer , si vous n'en avez pas nullou nilalors vous devrez simplement le remplacer par quelque chose d'autre, ce qui pourrait conduire à quelque chose pire qu'un crash (comme la corruption de données).

De nombreux cadres, en fait, embrassent les deux concepts; par exemple, dans .NET, un modèle fréquent que vous verrez est une paire de méthodes, une préfixée par le mot Try(comme TryGetValue). Dans le Trycas, la référence est définie sur sa valeur par défaut (généralement null), et dans l'autre cas, une exception est levée. Il n'y a rien de mal à l'une ou l'autre approche; les deux sont fréquemment utilisés dans les environnements qui les prennent en charge.

Tout dépend vraiment de la sémantique. Si nullest une valeur de retour valide, comme dans le cas général de la recherche d'une collection, alors retournez null. D'un autre côté, s'il ne s'agit pas d' une valeur de retour valide - par exemple, la recherche d'un enregistrement à l'aide d'une clé primaire provenant de votre propre base de données - alors le retour nullserait une mauvaise idée car l'appelant ne s'y attendrait pas et probablement ne le vérifiera pas.

Il est vraiment très simple de déterminer la sémantique à utiliser: est -il logique que le résultat d'une fonction ne soit pas défini? Si c'est le cas, vous pouvez renvoyer une référence nulle. Sinon, jetez une exception.


5
En fait, il y a des langues qui n'ont ni nul ni nul et qui "n'ont pas besoin de le remplacer par autre chose". Les références nulles impliquent qu'il pourrait y avoir quelque chose, ou qu'il pourrait ne pas y en avoir. Si vous demandez simplement à l'utilisateur de vérifier explicitement s'il y a quelque chose, vous avez résolu le problème. Voir haskell pour un exemple réel.
Johanna Larsson

3
@ErikKronberg, oui, "l'erreur d'un milliard de dollars" et toutes ces bêtises, le défilé de gens qui trottent et prétendent que cela est frais et fascinant ne finit jamais, c'est pourquoi le fil de commentaire précédent a été supprimé. Ces remplacements révolutionnaires que les gens ne manquent jamais d'évoquer sont toujours une variante de l'objet nul, de l'option ou du contrat, qui n'éliminent pas par magie l'erreur logique sous-jacente, ils la reportent ou la promeuvent, respectivement. , Cela est de toute façon de toute évidence une question sur les langages de programmation qui faire ont null, donc vraiment, Haskell est assez hors de propos ici.
Aaronaught

1
soutenez-vous sérieusement que ne pas exiger un test pour null est aussi bon que l'exiger?
Johanna Larsson

3
@ErikKronberg: Oui, je "soutiens sérieusement" que devoir tester pour null n'est pas particulièrement différent de (a) devoir concevoir chaque niveau d'une application autour du comportement d'un objet Null, (b) devoir faire correspondre des motifs Options tout le temps, ou (c) ne permettant pas à l'appelé de dire "je ne sais pas" et forçant une exception ou un crash. Il y a une raison pour laquelle cela nulla si bien duré si longtemps, et la majorité des gens qui disent le contraire semblent être des universitaires avec peu d'expérience dans la conception d'applications du monde réel avec des contraintes telles que des exigences incomplètes ou une cohérence éventuelle.
Aaronaught

3
@Aaronaught: Parfois, je souhaite qu'il y ait un bouton downvote pour les commentaires. Il n'y a aucune raison de déclamer comme ça.
Michael Shaw

11

La grande différence est que si vous omettez le code pour gérer les valeurs NULL, votre code continuera probablement à se bloquer à un stade ultérieur avec un message d'erreur non lié, où, comme pour les exceptions, l'exception serait levée au point d'échec initial (ouverture un fichier à lire dans votre exemple).


4
Le fait de ne pas gérer un NULL serait une ignorance volontaire de l'interface entre votre code et celui qui l'a retourné. Les développeurs qui commettraient cette erreur en feraient d'autres dans un langage qui ne fait pas de NULL.
Blrfl

@Blrfl, idéalement, les méthodes sont assez courtes, et il est donc facile de déterminer exactement où le problème se produit. Un bon débogueur peut généralement bien chasser une exception de référence nulle, même si le code est long. Que dois-je faire si j'essaie de lire un paramètre critique d'un registre, qui n'est pas là? Mon registre est foiré et je préfère échouer et ennuyer l'utilisateur, que de recréer silencieusement un nœud et de le définir par défaut. Et si un virus faisait ça? Donc, si j'obtiens une valeur nulle, dois-je lever une exception spécialisée ou simplement la laisser déchirer? Avec des méthodes courtes, quelle est la grande différence?
Job

@Job: Et si vous n'avez pas de débogueur? Vous vous rendez compte que 99,99% du temps, votre application s'exécutera dans un environnement de publication? Lorsque cela se produit, vous souhaiterez avoir utilisé une exception plus significative. Votre application devra peut-être encore échouer et ennuyer l'utilisateur, mais au moins elle produira des informations de débogage qui vous permettront de rechercher rapidement le problème, réduisant ainsi au minimum cette agacement.
Aaronaught

@Birfl, parfois je ne veux pas gérer le cas où un null serait naturel à retourner. Par exemple, supposons que j'ai un conteneur mappant les clés aux valeurs. Si ma logique garantit que je n'essaye jamais de lire des valeurs que je n'ai pas stockées en premier, alors je ne devrais jamais obtenir un retour nul. Dans ce cas, je préfère de loin avoir une exception qui fournit autant d'informations que possible pour indiquer ce qui s'est mal passé, plutôt que de retourner un null pour échouer mystérieusement ailleurs dans le programme.
Winston Ewert

En d'autres termes, à quelques exceptions près, je dois explicitement gérer le cas inhabituel, sinon mon programme meurt immédiatement. Avec des références nulles si le code ne gère pas explicitement le cas inhabituel, il essaiera de boiter. Je pense qu'il vaut mieux prendre le cas d'échec maintenant.
Winston Ewert

8

Parce que les valeurs nulles ne sont pas une partie nécessaire d'un langage de programmation et sont une source cohérente de bogues. Comme vous le dites, l'ouverture d'un fichier peut entraîner un échec, qui peut être communiqué en retour sous la forme d'une valeur de retour nulle ou via une exception. Si les valeurs nulles n'étaient pas autorisées, il existe un moyen unique et cohérent de communiquer l'échec.

En outre, ce n'est pas le problème le plus courant avec les valeurs NULL. La plupart des gens se souviennent de vérifier null après avoir appelé une fonction qui peut la renvoyer. Le problème surgit beaucoup plus dans votre propre conception en permettant aux variables d'être nulles à différents moments de l'exécution de votre programme. Vous pouvez concevoir votre code de telle sorte que les valeurs nulles ne soient jamais autorisées, mais si null n'était pas autorisé au niveau de la langue, rien de tout cela ne serait nécessaire.

Cependant, dans la pratique, vous auriez encore besoin d'un moyen de signifier si une variable est ou non initialisée. Vous auriez alors une forme de bogue dans laquelle votre programme ne se bloque pas, mais continue à la place en utilisant une valeur par défaut éventuellement invalide. Honnêtement, je ne sais pas ce qui est mieux. Pour mon argent, j'aime planter tôt et souvent.


Considérez une sous-classe non initialisée avec une chaîne d'identification, et toutes ses méthodes lèvent des exceptions. Si l'un d'eux se présente, vous savez ce qui s'est passé. Sur les systèmes embarqués avec une mémoire limitée, la version de production de l'usine non initialisée peut simplement retourner null.
Jim Balter

3
@ Jim Balter: Je suppose que je ne sais pas comment cela pourrait vraiment aider dans la pratique. Dans tout programme non trivial, vous devrez à un moment donné gérer des valeurs qui peuvent ne pas être initialisées. Il doit donc y avoir un moyen de signifier une valeur par défaut. En tant que tel, vous devez toujours vérifier cela avant de continuer. Ainsi, au lieu d'un crash potentiel, vous travaillez désormais potentiellement avec des données non valides.
Ed S.

1
Vous savez également ce qui s'est passé lorsque vous obtenez nullune valeur de retour: les informations que vous avez demandées n'existent pas. Il n'y a aucune différence. Dans les deux cas, l'appelant doit valider la valeur de retour s'il a réellement besoin d'utiliser ces informations dans un but particulier. Qu'il s'agisse de valider un null, un objet nul ou une monade ne fait aucune différence pratique.
Aaronaught

1
@ Jim Balter: D'accord, je ne vois toujours pas la différence pratique, et je ne vois pas comment cela rend la vie plus facile aux gens qui écrivent de vrais programmes en dehors du monde universitaire. Cela ne veut pas dire qu'il n'y a pas d'avantages, simplement qu'ils ne me semblent pas évidents.
Ed S.

1
J'ai expliqué la différence pratique avec une classe non initialisée deux fois - elle identifie l'origine de l'élément nul - ce qui permet de localiser le bogue lorsqu'une déréférence de null se produit. Quant aux langages conçus autour de paradigmes sans null, ils évitent le problème dès le départ - ils offrent des moyens alternatifs de programmation. Qui évitent les variables non initialisées; cela peut sembler difficile à saisir si vous ne les connaissez pas. La programmation structurée, qui évite également une grande classe de bogues, était également considérée comme «académique».
Jim Balter

5

Tony Hoare, qui a créé l'idée d'une référence nulle en premier lieu, l'appelle son erreur d'un million de dollars .

Le problème ne concerne pas les références nulles en soi, mais le manque de vérification de type appropriée dans la plupart des langues sûres (sinon) de type.

Ce manque de prise en charge, de la langue, signifie que les bogues "null-bugs" peuvent se cacher dans le programme pendant une longue période avant d'être détectés. Telle est la nature des bugs, bien sûr, mais les "null-bugs" sont désormais connus pour être évitables.

Ce problème est particulièrement présent en C ou C ++ (par exemple) à cause de l'erreur "dure" qu'il provoque (un crash du programme, immédiat, sans récupération élégante).

Dans d'autres langues, il y a toujours la question de savoir comment les gérer.

En Java ou C #, vous obtiendriez une exception si vous tentez d'appeler une méthode sur une référence nulle, et cela pourrait être correct. Et donc la plupart des programmeurs Java ou C # sont habitués à cela et ne comprennent pas pourquoi on voudrait faire autrement (et rire de C ++).

Dans Haskell, vous devez explicitement fournir une action pour le cas nul, et donc les programmeurs Haskell se moquent de leurs collègues, car ils l'ont bien compris (non?).

C'est, en fait, l'ancien débat sur les codes d'erreur / exceptions, mais cette fois avec une valeur sentinelle au lieu d'un code d'erreur.

Comme toujours, celui qui est le plus approprié dépend vraiment de la situation et de la sémantique que vous recherchez.


Précisément. nullest vraiment mauvais, car il subvertit le système de type . (Certes, l'alternative a un inconvénient dans sa verbosité, au moins dans certaines langues.)
Escargot mécanique

2

Vous ne pouvez pas joindre un message d'erreur lisible par un humain à un pointeur nul.

(Vous pouvez cependant laisser un message d'erreur dans un fichier journal.)

Dans certaines langues / environnements qui permettent l' arithmétique des pointeurs, si l' un des arguments de pointeur est nul et il est permis dans le calcul, le résultat serait invalide , non nul pointeur. (*) Plus de pouvoir pour vous.

(*) Cela se produit souvent dans la programmation COM , où si vous essayez d'appeler dans une méthode d'interface mais que le pointeur d'interface est nul, cela entraînerait un appel à une adresse invalide qui est presque, mais pas tout à fait, exactement différente de zéro.


2

Renvoyer NULL (ou zéro numérique ou booléen faux) pour signaler une erreur est incorrect à la fois techniquement et conceptuellement.

Techniquement, vous obligez le programmeur à vérifier immédiatement la valeur de retour , au point exact où elle est retournée. Si vous ouvrez vingt fichiers d'affilée et que la signalisation d'erreur se fait en retournant NULL, le code consommateur doit vérifier chaque fichier lu individuellement et sortir de toutes les boucles et constructions similaires. Ceci est une recette parfaite pour le code encombré. Si, toutefois, vous choisissez de signaler l'erreur en lançant une exception, le code consommateur peut choisir de gérer l'exception immédiatement ou de la laisser remonter autant de niveaux que nécessaire, même entre les appels de fonction. Cela rend le code beaucoup plus propre.

Conceptuellement, si vous ouvrez un fichier et que quelque chose ne va pas, le retour d'une valeur (même NULL) est incorrect. Vous n'avez rien à retourner, car votre opération n'est pas terminée. Renvoyer NULL est l'équivalent conceptuel de "J'ai réussi à lire le fichier, et voici ce qu'il contient - rien". Si c'est ce que vous voulez exprimer (c'est-à-dire si NULL a un sens comme résultat réel pour l'opération en question), alors renvoyez NULL, mais si vous voulez signaler une erreur, utilisez des exceptions.

Historiquement, des erreurs ont été signalées de cette façon car les langages de programmation comme C n'ont pas de gestion des exceptions intégrée dans le langage, et la manière recommandée (en utilisant de longs sauts) est un peu velue et un peu contre-intuitive.

Il y a aussi un côté maintenance au problème: à quelques exceptions près, vous devez écrire du code supplémentaire pour gérer l'échec; si vous ne le faites pas, le programme échouera tôt et durement (ce qui est bien). Si vous retournez NULL pour signaler des erreurs, le comportement par défaut du programme est d'ignorer l'erreur et de continuer jusqu'à ce qu'il entraîne d'autres problèmes sur la route - données corrompues, segfaults, NullReferenceExceptions, selon la langue. Pour signaler l'erreur tôt et fort, vous devez écrire du code supplémentaire et deviner quoi: c'est la partie qui est laissée de côté lorsque vous êtes dans un délai serré.


1

Comme déjà souligné, de nombreuses langues ne convertissent pas la déréférence d'un pointeur nul en exception capturable. Faire cela est une astuce relativement moderne. Lorsque le problème du pointeur nul a été reconnu pour la première fois, aucune exception n'avait encore été inventée.

Si vous autorisez les pointeurs nuls comme cas valide, il s'agit d'un cas spécial. Vous avez besoin d'une logique de traitement des cas spéciaux, souvent dans de nombreux endroits différents. C'est une complexité supplémentaire.

Qu'il s'agisse de pointeurs potentiellement nuls ou non, si vous n'utilisez pas de levées d'exceptions pour gérer les cas exceptionnels, vous devez gérer ces cas exceptionnels d'une autre manière. En règle générale, chaque appel de fonction doit faire l'objet de vérifications pour ces cas exceptionnels, soit pour empêcher l'appel de fonction d'être appelé de manière inappropriée, soit pour détecter le cas d'échec lorsque la fonction se termine. C'est une complexité supplémentaire qui peut être évitée en utilisant des exceptions.

Plus de complexité signifie généralement plus d'erreurs.

Les alternatives à l'utilisation de pointeurs nuls dans les structures de données (par exemple pour marquer le début / la fin d'une liste chaînée) incluent l'utilisation d'éléments sentinelles. Ceux-ci peuvent donner la même fonctionnalité avec beaucoup moins de complexité. Cependant, il peut y avoir d'autres façons de gérer la complexité. Une façon consiste à encapsuler le pointeur potentiellement nul dans une classe de pointeur intelligent afin que les vérifications nulles ne soient nécessaires qu'en un seul endroit.

Que faire du pointeur nul lorsqu'il est détecté? Si vous ne pouvez pas intégrer la gestion des cas exceptionnels, vous pouvez toujours lever une exception et déléguer efficacement cette gestion des cas spéciaux à l'appelant. Et c'est exactement ce que font certaines langues par défaut lorsque vous déréférencez un pointeur nul.


1

Spécifique au C ++, mais il y a des références nulles car la sémantique nulle en C ++ est associée aux types de pointeurs. Il est tout à fait raisonnable qu'une fonction d'ouverture de fichier échoue et renvoie un pointeur nul; en fait, la fopen()fonction fait exactement cela.


En effet, si vous vous retrouvez avec une référence nulle en C ++, c'est parce que votre programme est déjà cassé .
Kaz Dragon

1

Cela dépend de la langue.

Par exemple, Objective-C vous permet d'envoyer un message à un objet null (nil) sans problème. Un appel à zéro renvoie également zéro et est considéré comme une fonction de langue.

Personnellement, j'aime ça, car vous pouvez compter sur ce comportement et éviter toutes ces if(obj == null)constructions imbriquées alambiquées .

Par exemple:

if (myObject != nil && [myObject doSomething])
{
    ...
}

Peut être raccourci à:

if ([myObject doSomething])
{
    ...
}

En bref, cela rend votre code plus lisible.


0

Je ne me soucie pas de savoir si vous retournez une valeur nulle ou lève une exception, tant que vous la documentez.


Et aussi nommez- le: GetAddressMaybeou GetSpouseNameOrNull.
rwong

-1

Une référence Null se produit généralement parce que quelque chose dans la logique du programme a été manqué, c'est-à-dire: vous êtes arrivé à une ligne de code sans passer par la configuration requise pour ce bloc de code.

D'un autre côté, si vous lancez une exception pour quelque chose, cela signifie que vous avez reconnu qu'une situation particulière pouvait se produire dans le fonctionnement normal du programme et qu'elle était en cours de traitement.


2
Une référence nulle peut "se produire" pour un grand nombre de raisons, très peu d'entre elles se rapportant à tout ce qui est manquant. Peut-être que vous le confondez également avec une exception de référence nulle .
Aaronaught

-1

Les références nulles sont souvent très utiles: par exemple, un élément dans une liste chaînée peut avoir un successeur ou aucun successeur. L'utilisation nullpour «aucun successeur» est parfaitement naturelle. Ou, une personne peut avoir un conjoint ou non - utiliser nullpour «personne n'a pas de conjoint» est parfaitement naturel, beaucoup plus naturel que d'avoir une valeur spéciale «n'a pas de conjoint» à laquelle le Person.Spousemembre pourrait se référer.

Mais: de nombreuses valeurs ne sont pas facultatives. Dans un programme OOP typique, je dirais que plus de la moitié des références ne peuvent pas être nullaprès l'initialisation, sinon le programme échouera. Sinon, le code devrait être criblé de if (x != null)chèques. Alors, pourquoi chaque référence devrait-elle être annulable par défaut? Cela devrait vraiment être l'inverse: les variables doivent être non nulles par défaut, et vous devez explicitement dire "oh, et cette valeur peut l'être nullaussi".


y a-t-il quelque chose de cette discussion que vous aimeriez ajouter à votre réponse? Ce fil de commentaires s'est un peu échauffé et nous aimerions le nettoyer. Toute discussion approfondie doit être prise pour discuter .

Au milieu de toutes ces querelles, il aurait pu y avoir un ou deux points d'intérêt réel. Malheureusement, je n'ai pas vu le commentaire de Mark jusqu'à ce que j'élague la discussion prolongée. À l'avenir, veuillez signaler votre réponse à l'attention des modérateurs si vous souhaitez conserver les commentaires jusqu'à ce que vous ayez le temps de les examiner et de modifier votre réponse en conséquence.
Josh K

-1

Votre question est formulée de manière confuse. Voulez-vous dire une exception de référence nulle (qui est en fait causée par une tentative de référence null)? La raison évidente de ne pas vouloir ce type d'exception est qu'elle ne vous donne aucune information sur ce qui n'a pas fonctionné, ni même quand - la valeur aurait pu être définie sur null à tout moment du programme. Vous écrivez "Si je demande un accès en lecture à un fichier qui n'existe pas, je suis parfaitement heureux d'obtenir une exception ou une référence nulle" - mais vous ne devriez pas être parfaitement heureux d'obtenir quelque chose qui ne donne aucune indication sur la cause . Nulle part dans la chaîne "une tentative a été faite de déréférencer null" est-il fait mention de lecture ou de fichiers inexistants. Peut-être voulez-vous dire que vous seriez parfaitement heureux d'obtenir null comme valeur de retour d'un appel de lecture - c'est une question très différente, mais cela ne vous donne toujours aucune information sur les raisons de l'échec de la lecture; il'


Non, je ne parle pas d'une exception de référence nulle. Dans toutes les langues que je connais, les variables non initialisées mais déclarées sont une forme nulle.
davidk01

Mais votre question ne concernait pas les variables non initialisées. Et il existe des langages qui n'utilisent pas null pour les variables non initialisées, mais utilisent à la place des objets wrapper qui peuvent éventuellement contenir une valeur - Scala et Haskell, par exemple.
Jim Balter

1
"... mais à la place, utilisez des objets wrapper qui peuvent éventuellement contenir une valeur ." Ce qui n'est clairement rien comme un type nullable .
Aaronaught

1
Dans haskell, il n'y a pas de variable non initialisée. Vous pouvez potentiellement déclarer un IORef et l'amorcer avec None comme valeur initiale, mais c'est à peu près l'analogue de déclarer une variable dans un autre langage et de la laisser non initialisée, ce qui pose tous les mêmes problèmes. Travailler dans le noyau purement fonctionnel en dehors des programmeurs haskell de la monade IO n'a aucun recours aux types de référence, il n'y a donc pas de problème de référence nulle.
davidk01

1
Si vous avez une valeur "Missing" exceptionnelle, c'est exactement la même chose qu'une valeur "null" exceptionnelle - si vous avez les mêmes outils disponibles pour la gérer. C'est un "si" significatif. Vous avez besoin d'une complexité supplémentaire pour gérer ce cas de toute façon. Dans Haskell, la mise en correspondance de modèles et le système de types permettent de gérer cette complexité. Cependant, il existe d'autres outils pour gérer la complexité dans d'autres langues. Les exceptions sont un de ces outils.
Steve314
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.