Les références nulles sont-elles vraiment une mauvaise chose?

161

J'ai entendu dire que l'inclusion de références nulles dans les langages de programmation est une "erreur d'un milliard de dollars". Mais pourquoi? Bien sûr, ils peuvent provoquer des exceptions NullReferenceExceptions, mais alors quoi? Tout élément de la langue peut être une source d’erreurs s’il n’est pas utilisé correctement.

Et quelle est l'alternative? Je suppose au lieu de dire ceci:

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Vous pourriez dire ceci:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Mais comment est-ce mieux? Dans les deux cas, si vous oubliez de vérifier que le client existe, vous obtenez une exception.

Je suppose qu’une exception CustomerNotFoundException est un peu plus facile à déboguer qu’une exception NullReferenceException car elle est plus descriptive. Est-ce tout ce qu'il y a à faire?

Tim Goodman
la source
54
Je me méfierais de l'approche "si le client existe / obtient alors le client", dès que vous écrivez deux lignes de code comme ça, vous ouvrez la porte à des conditions de concurrence. Obtenez, puis vérifiez les exceptions nulls / catch est plus sûr.
Carson63000
26
Si seulement nous pouvions éliminer la source de toutes les erreurs d'exécution, notre code serait garanti d'être parfait! Si les références NULL en C # brisent votre cerveau, essayez des
indicateurs
Il existe cependant des opérateurs de navigation sans risque et de navigation sûre dans certaines langues
jk.
35
Nul en soi n'est pas mauvais. Un système de type qui ne fait aucune différence entre le type T et le type T + NULL est mauvais.
Ingo
27
Revenant à ma propre question quelques années plus tard, je suis maintenant totalement converti de l'approche Option / Maybe.
Tim Goodman

Réponses:

91

null est le mal

Il existe une présentation sur InfoQ à ce sujet: Références nulles: L’erreur du milliard de dollars de Tony Hoare

Type d'option

L'alternative à la programmation fonctionnelle consiste à utiliser un type Option pouvant contenir SOME valueou NONE.

Un bon article Le modèle «Option» qui traite du type Option et fournit une implémentation de celui-ci pour Java.

J'ai également trouvé un rapport de bogue pour Java sur ce problème: Ajoutez des types d'option Nice à Java pour empêcher NullPointerExceptions . La fonctionnalité demandée a été introduite dans Java 8.

Jonas
la source
5
Nullable <x> en C # est un concept similaire mais est loin d'être une implémentation du modèle d'option. La raison principale est que, dans .NET System.Nullable est limité aux types de valeur.
MattDavey
27
+1 pour le type d'option. Après s'être familiarisé avec Haskell, peut - être , les nuls commencent à paraître étranges ...
Andres F.
5
Le développeur peut commettre des erreurs n'importe où. Ainsi, au lieu d'une exception de pointeur null, vous obtiendrez une exception d'option vide. Comment ce dernier est-il meilleur que le précédent?
greenoldman
7
@greenoldman il n'y a pas de "exception d'option vide". Essayez d'insérer des valeurs NULL dans les colonnes de la base de données définies comme NOT NULL.
Jonas
36
@greenoldman Le problème avec les valeurs NULL est que tout peut être null, et en tant que développeur, vous devez être extrêmement prudent. Un Stringtype n'est pas vraiment une chaîne. C'est un String or Nulltype. Les langues qui adhèrent pleinement à l'idée de types d'options n'autorisent pas les valeurs NULL dans leur système de types, ce qui garantit que si vous définissez une signature de type qui nécessite une String(ou toute autre chose), elle doit avoir une valeur. Le Optiontype est là pour donner une représentation au niveau du type de choses qui peuvent ou non avoir une valeur, ainsi vous savez où vous devez gérer explicitement ces situations.
KChaloux
127

Le problème est qu’en théorie, tout objet pouvant avoir la valeur null et lancer une exception lorsque vous essayez de l’utiliser, votre code orienté objet est essentiellement une collection de bombes non explosées.

Vous avez raison de dire que la gestion des erreurs sans erreur peut être fonctionnellement identique aux ifinstructions à vérification nulle . Mais ce qui se passe quand quelque chose que vous vous ne pouvait pas convaincu peut - être une valeur nulle est, en fait, un nul? Kerboom. Quoi qu'il arrive, je suis prêt à parier que 1) cela ne sera pas gracieux et 2) que cela ne vous plaira pas.

Et ne rejetez pas la valeur de "facile à déboguer". Le code de production mature est une créature folle et tentaculaire; tout ce qui vous donne une idée plus précise de ce qui ne va pas et de ce qui peut vous faire économiser des heures de fouilles.

BlairHippo
la source
27
+1 Habituellement, les erreurs dans le code de production DOIVENT être identifiées le plus rapidement possible pour pouvoir être corrigées (généralement une erreur de données). Plus il est facile de déboguer, mieux c'est.
4
Cette réponse est la même si elle est appliquée à l'un des exemples du PO. Soit vous testez les conditions d'erreur, soit vous ne le faites pas, et vous les gérez avec élégance ou vous ne le faites pas. En d'autres termes, supprimer les références nulles du langage ne change pas les classes d'erreurs que vous allez rencontrer, cela changera simplement la manifestation de ces erreurs.
dash-tom-bang
19
+1 pour les bombes non explosées. Avec des références nulles, dire public Foo doStuff()ne signifie pas "faire des choses et renvoyer le résultat Foo", cela signifie "faire des choses et renvoyer le résultat Foo, OU renvoyer null, mais vous ne savez pas si cela se produira ou non". Le résultat est que vous devez vérifier NUL PARTOUT pour être certain. Vous pouvez également supprimer les valeurs NULL et utiliser un type ou un modèle spécial (comme un type de valeur) pour indiquer une valeur sans signification.
Matt Olenik
12
@greenoldman, votre exemple est doublement efficace pour démontrer notre argumentation en ce que l'utilisation de types primitifs (qu'ils soient nuls ou entiers) en tant que modèles sémantiques est une mauvaise pratique. Si vous avez un type pour lequel les nombres négatifs ne constituent pas une réponse valide, vous devez créer une nouvelle classe de types pour implémenter le modèle sémantique avec cette signification. Désormais, tout le code permettant de gérer la modélisation de ce type non négatif est localisé dans sa classe, isolé du reste du système, et toute tentative visant à créer une valeur négative peut être immédiatement interceptée.
Huperniketes
9
@greenoldman, vous continuez à prouver que les types primitifs sont inadéquats pour la modélisation. D'abord c'était sur les nombres négatifs. Maintenant, c'est fini l'opérateur de division quand zéro est le diviseur. Si vous savez que vous ne pouvez pas diviser par zéro, vous pouvez prédire que le résultat ne sera pas joli et vous devez vous assurer de tester et de fournir la valeur "valide" appropriée pour ce cas. Les développeurs de logiciels sont responsables de connaître les paramètres dans lesquels leur code fonctionne et de coder de manière appropriée. C'est ce qui distingue l'ingénierie du bricolage.
Huperniketes
50

L'utilisation de références null dans le code pose plusieurs problèmes.

Premièrement, il est généralement utilisé pour indiquer un état spécial . Plutôt que de définir une nouvelle classe ou une constante pour chaque état au fur et à mesure que les spécialisations sont effectuées, l'utilisation d'une référence null consiste à utiliser un type / valeur avec perte et généralisé .

Deuxièmement, le code de débogage devient plus difficile lorsqu'une référence null apparaît et que vous tentez de déterminer ce qui l'a généré , son état et sa cause, même si vous pouvez suivre son chemin d'exécution en amont.

Troisièmement, les références nulles introduisent des chemins de code supplémentaires à tester .

Quatrièmement, une fois que les références nulles sont utilisées en tant qu'états valides pour les paramètres ainsi que pour les valeurs renvoyées, la programmation défensive (pour les états causés par la conception) nécessite davantage de vérifications de références nulles à divers endroits … au cas où.

Cinquièmement, le langage d' exécution effectue déjà des vérifications de type lorsqu'il effectue une recherche par sélecteur sur la table de méthodes d'un objet. Vous dupliquez donc vos efforts en vérifiant si le type de l'objet est valide / invalide, puis en demandant au moteur d'exécution de vérifier le type de l'objet valide pour appeler sa méthode.

Pourquoi ne pas utiliser le modèle NullObject pour tirer parti de la vérification du moteur d'exécution afin qu'il appelle des méthodes NOP spécifiques à cet état (conformes à l'interface de l'état normal) tout en éliminant toute vérification supplémentaire des références null dans votre base de code?

Cela implique plus de travail en créant une classe NullObject pour chaque interface avec laquelle vous voulez représenter un état spécial. Mais au moins la spécialisation est isolée pour chaque état spécial, plutôt que le code dans lequel l'état pourrait être présent. IOW, le nombre de tests est réduit car vos méthodes utilisent moins de chemins d'exécution alternatifs .

Huperniketes
la source
7
... parce que déterminer qui a généré (ou plutôt n'a pas pris la peine de modifier ou d'initialiser) les données erronées qui remplissent votre objet supposé utile est tellement plus facile que le suivi d'une référence NULL? Je ne l'achète pas, bien que je sois la plupart du temps coincé dans des terres statiques.
dash-tom-bang
Les données erronées sont tellement plus rares que les références nulles. Particulièrement dans les langues gérées.
Dean Harding
5
-1 pour l'objet nul.
DeadMG
4
Les points (4) et (5) sont pleins, mais NullObject n'est pas un remède. Vous tomberez dans des pièges encore pires: des erreurs de logique (flux de travail). Lorsque vous connaissez EXACTEMENT la situation actuelle, cela peut aider, mais l’utiliser aveuglément (au lieu de nul) vous coûtera plus cher.
greenoldman
1
@ gnasher729, bien que le runtime d'Objective-C ne lève pas une exception null-pointeur comme le font Java et d'autres systèmes, l'utilisation des références null ne le rend pas meilleur pour les raisons que j'ai expliquées dans ma réponse. Par exemple, s'il n'y a pas de navigationController dans [self.navigationController.presentingViewController dismissViewControllerAnimated: YES completion: nil];le moteur d'exécution, il la traite comme une séquence de non-opérations, la vue n'est pas supprimée de l'écran et vous pouvez vous asseoir devant Xcode pour vous gratter la tête pendant des heures et tenter de comprendre pourquoi.
Huperniketes
48

Les nullités ne sont pas si mauvaises, à moins que vous ne les attendiez pas. Vous devriez avoir besoin de spécifier explicitement dans le code que vous attendez la valeur null, ce qui est un problème de conception de langage. Considère ceci:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Si vous essayez d'appeler des méthodes sur un Customer?, vous devriez obtenir une erreur de compilation. La raison pour laquelle d’autres langages ne font pas cela (IMO) est qu’ils ne fournissent pas le type de variable à modifier en fonction de la portée de celle-ci. Si le langage peut le gérer, le problème peut être entièrement résolu dans le système de type.

Il existe également un moyen fonctionnel de gérer ce problème, en utilisant des types d'options et Maybe, mais je ne le connais pas aussi bien. Je préfère cette méthode car un code correct ne devrait théoriquement nécessiter qu'un seul caractère ajouté pour être compilé correctement.

Note pour soi - pense à un nom
la source
11
Wow, c'est plutôt cool. En plus d'attraper beaucoup plus d'erreurs au moment de la compilation, cela m'évite d'avoir à vérifier la documentation pour savoir si GetByLastNamerenvoie null si non trouvé (par opposition à une exception) - je peux juste voir si elle retourne Customer?ou Customer.
Tim Goodman
14
@JBRWilkinson: Ceci est juste un exemple concocté pour montrer l'idée, pas un exemple d'une implémentation réelle. Je pense en fait que c'est une bonne idée.
Dean Harding
1
Vous avez raison de dire que ce n'est pas le motif d'objet null, cependant.
Dean Harding
13
Cela ressemble à ce que Haskell appelle un Maybe- A Maybe Customerest soit Just c(où cest a Customer) ou Nothing. Dans d'autres langues, cela s'appelle Option- voir d'autres réponses à cette question.
MatrixFrog
2
@SimonBarker: vous pouvez être sûr si Customer.FirstNameest de type String(par opposition à String?). C'est l'avantage réel: seules les variables / propriétés qui ont une valeur rien significative doivent être déclarées nullables.
Yogu
20

Il y a une foule d'excellentes réponses qui couvrent les symptômes malheureux de null, alors j'aimerais présenter un argument alternatif: Null est une faille dans le système de types.

Le but d'un système de types est de s'assurer que les différents composants d'un programme "s'emboîtent" correctement; un programme bien typé ne peut pas "dérober" un comportement indéfini.

Considérons un dialecte hypothétique de Java, ou quel que soit votre langage préféré à typage statique, dans lequel vous pouvez affecter la chaîne "Hello, world!"à n'importe quelle variable de n'importe quel type:

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

Et vous pouvez vérifier les variables comme suit:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

Il n’ya rien d’impossible à cela: quelqu'un pourrait concevoir un tel langage s’il le souhaite. La valeur spéciale ne doit pas nécessairement être "Hello, world!"- cela aurait pu être le nombre 42, le tuple (1, 4, 9), ou, disons null. Mais pourquoi feriez-vous cela? Une variable de type Foone devrait contenir que Foos - c'est tout l'intérêt du système de types! nulln'est pas un Fooplus que ce "Hello, world!"n'est. Pire, ce nulln'est une valeur d' aucun type, et vous ne pouvez rien y faire!

Le programmeur ne peut jamais être sûr qu'une variable détient réellement un Foo, et le programme non plus; afin d'éviter tout comportement indéfini, il doit vérifier les variables "Hello, world!"avant de les utiliser en tant que Foos. Notez que faire la vérification de chaîne dans le fragment précédent ne propage pas le fait que foo1 est vraiment un Foo- baraura probablement aussi sa propre vérification, juste pour être sûr.

Comparez cela à l'utilisation d'un Maybe/ Optiontype avec un motif correspondant:

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

À l'intérieur de la Just fooclause, vous et le programme savez à coup sûr que notre Maybe Foovariable contient réellement une Foovaleur: cette information est propagée dans la chaîne d'appels et barne nécessite aucune vérification. Parce que Maybe Fooest un type distinct de Foo, vous êtes obligé de gérer la possibilité qu'il pourrait contenir Nothing, de sorte que vous ne pouvez jamais être pris au dépourvu par un NullPointerException. Vous pouvez raisonner beaucoup plus facilement sur votre programme et le compilateur peut omettre les vérifications nulles en sachant que toutes les variables de type Foocontiennent réellement des Foos. Tout le monde gagne.

Doval
la source
Qu'en est-il des valeurs de type null dans Perl 6? Ils sont toujours nuls, sauf qu'ils sont toujours dactylographiés. Est-ce toujours un problème que vous pouvez écrire my Int $variable = Int, où Intest la valeur null de type Int?
Konrad Borowski
@xfix Je ne suis pas familier avec Perl, mais aussi longtemps que vous ne disposez pas d' un moyen de faire respecter this type only contains integerspar opposition à this type contains integers and this other value, vous aurez un problème chaque fois que vous ne voulez entiers.
Doval
1
+1 pour la correspondance de modèle. Avec le nombre de réponses ci-dessus mentionnant les types d'option, vous auriez pu penser que quelqu'un aurait mentionné le fait qu'une simple fonctionnalité de langage, telle que le filtrage de motifs, peut les rendre beaucoup plus intuitif que les valeurs nulles.
Jules
1
Je pense que la liaison monadique est encore plus utile que la correspondance de motif (bien que, bien sûr, bind pour Maybe soit implémenté à l'aide de la correspondance de motif). Je trouve ça amusant de voir des langages comme C # ajouter une syntaxe spéciale à de nombreuses choses ( ??, ?.etc.) pour des langages comme haskell qui implémentent des fonctions de bibliothèque. Je pense que c'est beaucoup plus soigné. null/ Nothingpropagation n'est en aucun cas "spécial", alors pourquoi devrions-nous apprendre plus de syntaxe pour l'avoir?
Sara
14

(Jeter mon chapeau sur le ring pour une vieille question;))

Le problème spécifique de null est qu'il interrompt le typage statique.

Si j'ai un Thing t, alors le compilateur peut garantir que je peux appeler t.doSomething(). Eh bien, sauf si tnull est à l'exécution. Maintenant, tous les paris sont ouverts. Le compilateur a dit que c'était OK, mais je découvre beaucoup plus tard que ce tn'est pas le cas doSomething(). Donc, plutôt que de pouvoir faire confiance au compilateur pour intercepter les erreurs de type, je dois attendre le temps d'exécution pour les intercepter. Je pourrais aussi bien utiliser Python!

Donc, dans un sens, null introduit le typage dynamique dans un système à typage statique, avec les résultats attendus.

La différence entre une division par zéro ou un log de négatif, etc. est que lorsque i = 0, il s'agit toujours d'un entier. Le compilateur peut toujours garantir son type. Le problème est que la logique applique mal cette valeur d'une manière qui n'est pas permise par la logique ... mais si la logique le permet, c'est à peu près la définition d'un bogue.

(Le compilateur peut résoudre certains de ces problèmes, BTW. Par exemple, marquer des expressions comme i = 1 / 0au moment de la compilation. Mais vous ne pouvez pas vous attendre à ce que le compilateur suive une fonction et veille à ce que tous les paramètres soient cohérents avec sa logique.)

Le problème pratique est que vous faites beaucoup de travail supplémentaire et que vous ajoutez des contrôles nuls pour vous protéger au moment de l'exécution, mais que se passe-t-il si vous en oubliez un? Le compilateur vous empêche d'attribuer:

Chaîne s = nouvel entier (1234);

Alors, pourquoi devrait-il permettre l’affectation à une valeur (null) qui interrompra les dé-références s?

En mélangeant "aucune valeur" avec des références "typées" dans votre code, vous mettez une charge supplémentaire sur les programmeurs. Et lorsque NullPointerExceptions se produit, les rechercher peut prendre encore plus de temps. Plutôt que de compter sur le typage statique pour dire «ceci est une référence à quelque chose d’attendu», vous laissez le langage dire «cela pourrait bien être une référence à quelque chose d’attendu».

Rob Y
la source
3
En C, une variable de type *intidentifie un int, ou NULL. De même en C ++, une variable de type *Widgetidentifie Widgetune chose dont le type dérive Widget, ou NULL. Le problème avec Java et les dérivés tels que C # est qu’ils utilisent l’emplacement de stockage Widgetpour signifier *Widget. La solution n’est pas de prétendre qu’un *Widgetne devrait pas pouvoir tenir NULL, mais bien d’avoir un Widgettype d’emplacement de stockage approprié .
Supercat
14

Un nullpointeur est un outil

pas un ennemi. Vous devez juste les utiliser correctement.

Pensez juste au temps qu'il faut pour trouver et résoudre un bogue de pointeur invalide typique , par rapport à la recherche et à la correction d'un bogue de pointeur nul. Il est facile de vérifier un pointeur contre null. C'est un PITA qui vérifie si un pointeur non nul pointe sur des données valides.

Si vous n'êtes toujours pas convaincu, ajoutez une bonne dose de multithreading à votre scénario, puis réfléchissez à nouveau.

Morale de l'histoire: Ne jetez pas les enfants avec de l'eau. Et ne blâmez pas les outils pour cet accident. L'homme qui a inventé le chiffre zéro il y a longtemps était assez malin, puisque ce jour-là, vous pouviez nommer le "rien". Le nullpointeur n'est pas si loin de ça.


EDIT: Bien que le NullObjectmodèle semble être une meilleure solution que les références pouvant être nulles, il introduit lui-même des problèmes:

  • Une référence tenant un NullObjectdevrait (selon la théorie) ne fait rien quand une méthode est appelée. Ainsi, des erreurs subtiles peuvent être introduites en raison de références non attribuées par erreur, qui sont maintenant garanties non nulles (yehaa!) Mais exécutent une action non désirée: rien. Avec un NPE, le problème est presque évident. Avec un NullObjectcomportement correct (mais faux), nous introduisons le risque que des erreurs soient détectées (trop) tardivement. Ce n'est pas accidentellement similaire à un problème de pointeur invalide et a des conséquences similaires: la référence pointe vers quelque chose qui ressemble à des données valides, mais ne le fait pas, introduit des erreurs de données et peut être difficile à localiser. Pour être honnête, dans ces cas, je préférerais sans hésiter un NPE qui échoue immédiatement, maintenant,sur une erreur logique / de données qui me frappe soudainement plus tard sur la route. Vous souvenez-vous de l'étude d'IBM sur le coût des erreurs en fonction du moment où elles sont détectées?

  • La notion de ne rien faire quand une méthode est appelée sur un NullObjectne tient pas, quand un getter de propriété ou une fonction est appelée, ou quand la méthode renvoie des valeurs via out. Disons que la valeur de retour est un intet nous décidons que "ne rien faire" signifie "retourner 0". Mais comment pouvons-nous être sûrs que 0 est le droit "rien" à être (non) retourné? Après tout, le NullObjectne devrait rien faire, mais lorsqu'on lui demande une valeur, il doit réagir d'une manière ou d'une autre. L'échec n'est pas une option: nous utilisons le NullObjectpour empêcher le NPE, et nous ne le négocierons sûrement pas contre une autre exception (non mis en œuvre, diviser par zéro, ...), n'est-ce pas? Alors, comment ne retournez-vous rien correctement lorsque vous devez retourner quelque chose?

Je ne peux pas aider, mais quand quelqu'un essaie d'appliquer le NullObjectmotif à chaque problème, cela ressemble plus à essayer de réparer une erreur en en faisant une autre. C'est sans aucun doute une solution utile et utile dans certains cas, mais ce n'est certainement pas la solution miracle pour tous les cas.

JensG
la source
Pouvez-vous présenter un argument pour pourquoi nulldevrait exister? Il est possible de traiter à la main n'importe quel défaut de langue avec "ce n'est pas un problème, mais ne commettez pas d'erreur".
Doval
À quelle valeur définiriez-vous un pointeur invalide?
JensG
Eh bien, vous ne leur attribuez aucune valeur, car vous ne devriez pas les autoriser du tout. Au lieu de cela, vous utilisez une variable d'un type différent, appelé Maybe Téventuellement, qui peut contenir l'une Nothingou l' autre ou une Tvaleur. Le point clé ici est que vous êtes obligé de vérifier si un Maybeproduit contient vraiment un mot Tavant que vous ne soyez autorisé à l'utiliser, et de fournir une ligne de conduite au cas où ce ne serait pas le cas. Et comme vous avez interdit les pointeurs non valides, vous ne devez plus jamais vérifier ce qui n’est pas un Maybe.
Doval
1
@JensG dans votre dernier exemple, le compilateur vous oblige-t-il à effectuer la vérification nulle avant chaque appel à t.Something()? Ou a-t-il un moyen de savoir que vous avez déjà effectué le contrôle et de ne pas vous le faire refaire? Et si vous faisiez la vérification avant de passer tà cette méthode? Avec le type Option, le compilateur sait si vous avez déjà effectué la vérification en regardant le type de t. Si c'est une option, vous ne l'avez pas cochée, alors que si c'est la valeur non enveloppée, alors vous l'avez.
Tim Goodman
1
Que retourne l'objet null quand on lui demande une valeur?
JensG
8

Les références nulles sont une erreur car elles autorisent un code non sensuel:

foo = null
foo.bar()

Il existe des alternatives si vous utilisez le système de types:

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

L'idée générale est de mettre la variable dans une boîte, et la seule chose à faire est de la déballer, en recourant de préférence à l'aide du compilateur comme celle proposée pour Eiffel.

Haskell l'a depuis le début ( Maybe), en C ++, vous pouvez en tirer parti boost::optional<T>mais vous pouvez toujours obtenir un comportement indéfini ...

Matthieu M.
la source
6
-1 pour "effet de levier"!
Marc C
8
Mais sûrement, beaucoup de constructions de programmation communes permettent un code absurde, non? La division par zéro est (sans doute) absurde, mais nous autorisons toujours la division et espérons que le programmeur se souviendra de vérifier que le diviseur est non nul (ou gère l'exception).
Tim Goodman
3
Je ne suis pas certain de ce que vous entendez par « à partir de zéro », mais Maybene se construit pas dans la langue de base, la définition se trouve être dans le prélude standard: data Maybe t = Nothing | Just t.
fredoverflow
4
@FredOverflow: étant donné que le prélude est chargé par défaut, je considérerais cela "de zéro", le fait que Haskell possède un système de types assez étonnant pour permettre à cette fin (et d'autres) d'être définie avec les règles générales du langage n'est qu'un bonus :)
Matthieu M.
2
@TimGoodman Alors que la division par zéro peut ne pas être le meilleur exemple pour tous les types de valeurs numériques (IEEE 754 définit un résultat pour la division par zéro), dans les cas où une exception serait levée, au lieu d'avoir des valeurs de type Tdivisées par d'autres valeurs de type T, vous limiteriez l'opération aux valeurs de type Tdivisées par les valeurs de type NonZero<T>.
kbolino
8

Et quelle est l'alternative?

Types facultatifs et correspondance de modèle. Puisque je ne connais pas le C #, voici un morceau de code rédigé dans un langage fictif appelé Scala # :-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}
fredoverflow
la source
6

Le problème avec les valeurs nulles est que les langages qui leur permettent de vous forcer à programmer en défensive contre elles. Il faut beaucoup d’efforts (bien plus que d’utiliser des blocs de défense défensifs) pour s’assurer que

  1. les objets que vous attendez ne pas être nuls ne sont en effet jamais nuls, et
  2. que vos mécanismes de défense traitent effectivement avec tous les NPE potentiels.

Ainsi, en effet, les valeurs nulles deviennent une opération coûteuse.

luis.espinal
la source
1
Cela pourrait être utile si vous incluiez un exemple où il faut beaucoup plus d'efforts pour traiter les valeurs nulles. Dans mon exemple, certes excessivement simplifié, l'effort est à peu près le même dans les deux cas. Je ne suis pas en désaccord avec vous, je trouve que des exemples de (pseudo) codes précis brossent un tableau plus clair que les déclarations générales.
Tim Goodman
2
Ah, je ne me suis pas expliqué clairement. Ce n’est pas qu’il est plus difficile de traiter avec des valeurs nulles. C'est qu'il est extrêmement difficile (voire impossible) de garantir que tous les accès à des références potentiellement nulles sont déjà protégés. C'est-à-dire que dans un langage qui autorise les références nulles, il est tout simplement impossible de garantir qu'un morceau de code de taille arbitraire soit exempt d'exceptions de pointeur nul, non sans quelques difficultés et efforts majeurs. C'est ce qui rend les références nulles une chose chère. Il est extrêmement coûteux de garantir l’absence complète d’exceptions de pointeur nul.
luis.espinal
4

La question est de savoir dans quelle mesure votre langage de programmation tente de prouver l'exactitude de votre programme avant de l'exécuter. Dans un langage typé statiquement, vous prouvez que vous avez les types corrects. En passant aux valeurs par défaut des références non nullables (avec des références facultatives Nullable), vous pouvez éliminer bon nombre des cas où la valeur null a été passée et elle ne devrait pas l'être. La question qui se pose est de savoir si l’effort supplémentaire de traitement des références non nullables vaut l’avantage en termes de correction du programme.

Winston Ewert
la source
4

Le problème n'est pas tellement nul, c'est que vous ne pouvez pas spécifier un type de référence non nul dans beaucoup de langues modernes.

Par exemple, votre code pourrait ressembler à

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    egg.Crack();
    MixingBowl.Mix(egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

Lorsque la référence de classe est transmise, la programmation défensive vous incite à vérifier à nouveau tous les paramètres, même si vous venez juste de les vérifier à l’avance, en particulier lors de la construction d’unités réutilisables à tester. La même référence pourrait facilement être vérifiée à zéro 10 fois dans la base de code!

Ce ne serait pas mieux si vous pouviez avoir un type nullable normal quand vous obtenez la chose, traitez null puis, puis passez-le comme un type non nullable à toutes vos petites fonctions et classes d’aide sans chercher null pour null temps?

C’est l’intérêt de la solution Option, non pas de vous permettre d’activer les états d’erreur, mais de permettre la conception de fonctions qui, implicitement, ne peuvent pas accepter les arguments nuls. Que ce soit ou non l'implémentation "par défaut" des types est nullable ou non-nullable est moins important que la flexibilité d'avoir les deux outils disponibles.

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

Ceci est également classé comme la fonctionnalité # 2 la plus votée pour C # -> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c

Michael Parker
la source
Je pense qu’un autre point important concernant l’option <T> est qu’il s’agit d’une monade (et donc également d’un endofoncteur), de sorte que vous pouvez composer des calculs complexes sur des flux de valeur contenant des types facultatifs sans vérifier explicitement Nonechaque étape du processus.
Sara
3

Null est le mal. Cependant, l'absence d'un nul peut être un plus grand mal.

Le problème est que dans le monde réel, vous vous retrouvez souvent dans une situation où vous ne disposez pas de données. Votre exemple de la version sans null peut encore exploser - soit vous avez commis une erreur de logique et oublié de vérifier Goodman, soit peut-être que Goodman s'est marié entre vous lorsque vous avez vérifié et que vous l'avez vérifiée. (Il est utile d’évaluer la logique si vous pensez que Moriarty surveille chaque partie de votre code et tente de vous faire trébucher.)

Que fait la recherche de Goodman quand elle n'est pas là? Sans null, vous devez retourner une sorte de client par défaut - et maintenant vous vendez des choses à ce défaut.

Fondamentalement, il s’agit de savoir s’il est plus important que le code fonctionne, quel qu’il soit ou qu’il fonctionne correctement. Si un comportement inapproprié est préférable à aucun comportement, vous ne voulez pas de valeurs NULL. (Pour un exemple de ce qui pourrait être le bon choix, considérons le premier lancement de l’Ariane V. Une erreur non capturée / 0 a provoqué un virage serré de la fusée et l’autodestruction a été tirée lorsqu’elle s’est effondrée à cause de cela. La valeur il essayait de calculer en fait ne servait plus à rien dès l'instant où le booster était allumé - il aurait été mis en orbite malgré la routine de retour des ordures.)

Au moins 99 fois sur 100, je choisirais d'utiliser des valeurs nulles. Je voudrais sauter de joie à la note à la version de soi d'eux, cependant.

Loren Pechtel
la source
1
C'est une bonne réponse. La construction null dans la programmation n'est pas le problème, ce sont toutes les instances de valeurs null. Si vous créez un objet par défaut, toutes vos valeurs NULL vont éventuellement être remplacées par ces valeurs par défaut et votre interface utilisateur, votre base de données et, entre les deux, seront remplis par ceux qui ne sont probablement plus utiles. Mais vous dormirez la nuit car au moins votre programme ne plante pas, je suppose.
Chris McCall
2
Vous supposez que nullc'est le seul (voire même préférable) moyen de modéliser l'absence de valeur.
Sara
1

Optimiser pour le cas le plus commun.

Il est fastidieux de devoir vérifier tout le temps la valeur null - vous voulez pouvoir obtenir simplement l'objet Client et le manipuler.

Dans le cas normal, cela devrait fonctionner correctement: vous effectuez la recherche, récupérez l'objet et utilisez-le.

Dans le cas exceptionnel , où vous recherchez (au hasard) un client par son nom, sans savoir si cet enregistrement / cet objet existe, vous avez besoin d'une indication indiquant que cela a échoué. Dans cette situation, la solution consiste à lever une exception RecordNotFound (ou à laisser le fournisseur SQL inférieur le faire pour vous).

Si vous ne savez pas si vous pouvez faire confiance aux données qui arrivent (le paramètre), peut-être parce qu'elles ont été entrées par un utilisateur, vous pouvez également fournir le «TryGetCustomer (nom, client)». modèle. Référence croisée avec int.Parse et int.TryParse.

JBRWilkinson
la source
J'affirmerais que vous ne savez jamais si vous pouvez faire confiance aux données qui arrivent, car elles entrent dans une fonction et sont de type nullable. Le code pourrait être restructuré pour rendre l’entrée totalement différente. Donc, s'il y a une possibilité nulle, vous devez toujours vérifier. Vous vous retrouvez donc avec un système dans lequel chaque variable est vérifiée comme nulle chaque fois avant son accès. Les cas où cette case n'est pas cochée deviennent le pourcentage d'échec de votre programme.
Chris McCall
0

Les exceptions ne sont pas eval!

Ils sont là pour une raison. Si votre code fonctionne mal, il existe un motif de génie, appelé exception , qui vous indique que quelque chose ne va pas.

En évitant d'utiliser des objets nuls, vous masquez une partie de ces exceptions. Je ne parle pas de l'exemple OP où il convertit l'exception du pointeur null en une exception bien typée. Cela pourrait en fait être une bonne chose, car cela améliore la lisibilité. Cependant, lorsque vous prenez la solution de type Option , comme l'a souligné @Jonas, vous cachez des exceptions.

Que dans votre application de production, au lieu d'exception à être levée lorsque vous cliquez sur le bouton pour sélectionner l'option vide, rien ne se passe. Au lieu d'exception de pointeur nul, une exception serait générée et nous obtiendrions probablement un rapport de cette exception (comme dans de nombreuses applications de production, nous avons un tel mécanisme), et nous pourrions le réparer.

Rendre votre code à l'épreuve des balles en évitant les exceptions est une mauvaise idée, vous obliger à coder à l'épreuve des balles en corrigeant une exception, voilà le chemin que je choisirais.

Ilya Gazman
la source
1
J'en sais un peu plus sur le type Option maintenant que lorsque j'ai posté la question il y a quelques années, car je les utilise régulièrement à Scala. Le point important de Option est qu'il permet d'assigner Noneà quelque chose qui n'est pas une option une erreur de compilation, et d'utiliser également un Optioncomme s'il avait une valeur sans vérifier au préalable qu'il s'agit d'une erreur de compilation. Les erreurs de compilation sont certainement plus agréables que les exceptions d'exécution.
Tim Goodman
Par exemple: Vous ne pouvez pas dire s: String = None, vous devez dire s: Option[String] = None. Et si sest une option, vous ne pouvez pas le dire s.length, mais vous pouvez vérifier qu’elle a une valeur et extraire la valeur en même temps, avec correspondance de motif:s match { case Some(str) => str.length; case None => throw RuntimeException("Where's my value?") }
Tim Goodman
Malheureusement, vous pouvez toujours le dire à Scala s: String = null, mais c’est le prix de l’interopérabilité avec Java. Nous interdisons généralement ce genre de chose dans notre code Scala.
Tim Goodman
2
Cependant, je souscris à l'observation générale selon laquelle les exceptions (utilisées correctement) ne sont pas une mauvaise chose, et "réparer" une exception en l'avalant est généralement une idée terrible. Mais avec le type Option, l’objectif est de transformer les exceptions en erreurs de compilation, ce qui est préférable.
Tim Goodman
@TimGoodman est-il une simple tactique ou peut-il être utilisé dans d'autres langages tels que Java et ActionScript 3? En outre, il serait bien que vous puissiez modifier votre réponse avec un exemple complet.
Ilya Gazman
0

Nullssont problématiques car elles doivent être explicitement vérifiées, mais le compilateur ne peut pas vous avertir que vous avez oublié de les vérifier. Seule une analyse statique fastidieuse peut vous le dire. Heureusement, il existe plusieurs bonnes alternatives.

Prenez la variable hors de la portée. Bien trop souvent, nullest utilisé comme remplaçant lorsqu'un programmeur déclare une variable trop tôt ou la garde trop longtemps. La meilleure approche consiste simplement à supprimer la variable. Ne le déclarez pas avant d'avoir une valeur valide à y inscrire. Ce n'est pas une restriction aussi difficile que vous ne le pensez et cela rend votre code beaucoup plus propre.

Utilisez le modèle d'objet null. Parfois, une valeur manquante est un état valide pour votre système. Le modèle d'objet nul est une bonne solution ici. Cela évite le recours à des contrôles explicites constants, mais vous permet néanmoins de représenter un état nul. Certaines personnes prétendent qu'il ne s'agit pas de "masquer une condition d'erreur", car dans ce cas, un état nul est un état sémantique valide. Cela étant dit, vous ne devriez pas utiliser ce modèle lorsqu'un état null n'est pas un état sémantique valide. Vous devriez juste prendre votre variable hors de portée.

Utilisez une option / peut-être. Tout d’abord, cela permet au compilateur de vous avertir que vous devez rechercher une valeur manquante, mais cela ne se limite pas à remplacer un type de contrôle explicite par un autre. En utilisant Options, vous pouvez chaîner votre code et poursuivre comme si votre valeur existait, sans avoir besoin de vérifier réellement jusqu'à la dernière minute. Dans Scala, votre exemple de code ressemblerait à quelque chose comme:

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

Sur la deuxième ligne, où nous construisons le message génial, nous ne vérifions pas explicitement si le client a été trouvé, et la beauté est que nous n’avons pas besoin de le faire . Ce n'est pas avant la toute dernière ligne, qui peut être plusieurs lignes plus tard, ou même dans un module différent, où nous nous intéressons explicitement à ce qui se passe si le Optionest None. Non seulement cela, si nous avions oublié de le faire, il n'aurait pas vérifié le type. Même alors, cela se fait de manière très naturelle, sans ifdéclaration explicite .

Comparez cela à un null, où il vérifie parfaitement le type, mais où vous devez effectuer un contrôle explicite à chaque étape ou lorsque votre application entière explose au moment de l'exécution, si vous avez de la chance lors de votre exercice de tests unitaires. Cela ne vaut tout simplement pas la peine.

Karl Bielefeldt
la source
0

Le problème central de NULL est qu’il rend le système peu fiable. En 1980, Tony Hoare dans le journal dédié à son prix Turing écrivait:

Ainsi, le meilleur de mes conseils aux concepteurs et concepteurs d’ADA a été ignoré. … Ne laissez pas cette langue dans son état actuel être utilisée dans des applications où la fiabilité est essentielle, telles que les centrales nucléaires, les missiles de croisière, les systèmes d'alerte précoce, les systèmes de défense antimissile antiballistique. La prochaine fusée à s'égarer à la suite d'une erreur de langage de programmation risque de ne pas être une fusée spatiale exploratoire lors d'un voyage inoffensif à Vénus: il peut s'agir d'une tête nucléaire qui explose dans l'une de nos propres villes. Un langage de programmation peu fiable générant des programmes non fiables constitue un risque bien plus grand pour notre environnement et notre société que les voitures non sécurisées, les pesticides toxiques ou les accidents survenus dans les centrales nucléaires. Soyez vigilant pour réduire le risque, ne pas l'augmenter.

Le langage ADA a beaucoup changé depuis, mais de tels problèmes persistent en Java, en C # et dans de nombreux autres langages populaires.

Les développeurs ont le devoir de créer des contrats entre un client et un fournisseur. Par exemple, en C #, comme en Java, vous pouvez Genericsréduire l’impact de la Nullréférence en créant en lecture seule NullableClass<T>(deux options):

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

et ensuite l'utiliser comme

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

ou utilisez deux styles avec les méthodes d'extension C #:

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

La différence est significative. En tant que client d'une méthode, vous savez à quoi vous attendre. Une équipe peut avoir la règle:

Si une classe n'est pas de type NullableClass, son instance ne doit pas être nulle .

L’équipe peut renforcer cette idée en utilisant Conception par contrat et vérification statique au moment de la compilation, par exemple avec les conditions préalables suivantes:

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

ou pour une ficelle

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

Cette approche peut considérablement augmenter la fiabilité des applications et la qualité du logiciel. Design by Contract est un cas de logique Hoare , qui a été peuplé par Bertrand Meyer dans son célèbre livre Construction de logiciels orientés objet et en langage Eiffel en 1988, mais il n’est pas utilisé de manière non valide dans l’élaboration de logiciels modernes.

Artru
la source
0

Non.

L'échec d'une langue à prendre en compte une valeur spéciale, comme nullc'est le problème de la langue. Si vous examinez des langages comme Kotlin ou Swift , qui obligent le rédacteur à traiter explicitement de la possibilité d'une valeur nulle à chaque rencontre, il n'y a pas de danger.

Dans des langages comme celui en question, le langage permet nulld’être une valeur de tout type -ish , mais ne fournit aucune construction permettant de le reconnaître ultérieurement, ce qui conduit à des événements nullinattendus (surtout lorsqu’ils vous sont transmis d’autre part), et ainsi vous obtenez des accidents. C’est le mal: vous laisser utiliser nullsans vous en préoccuper.

Ben Leggiero
la source
-1

En gros, l'idée centrale est que les problèmes de référence de pointeurs nuls doivent être interceptés au moment de la compilation plutôt qu'à l'exécution. Donc, si vous écrivez une fonction qui prend un objet et que vous ne voulez pas que quelqu'un l'appelle avec des références nulles, le système de type devrait vous permettre de spécifier cette exigence afin que le compilateur puisse l'utiliser pour garantir que l'appel de votre fonction ne serait jamais effectué. compiler si l'appelant ne répond pas à vos besoins. Cela peut être fait de différentes manières, par exemple en décorant vos types ou en utilisant des types d’options (également appelés types nullables dans certaines langues), mais c’est évidemment beaucoup plus de travail pour les concepteurs de compilateur.

Je pense qu'il est compréhensible que dans les années 60 et 70, le concepteur de compilateur ne se soit pas plié en bloc pour mettre en œuvre cette idée, car les machines étaient limitées en ressources, les compilateurs devaient rester relativement simples et personne ne savait vraiment à quel point 40 années sur la ligne.

Shital Shah
la source
-1

J'ai une simple objection contre null:

Il casse complètement la sémantique de votre code en introduisant une ambiguïté .

Souvent, vous vous attendez à ce que le résultat soit d'un certain type. Ceci est sémantiquement correct. Dites, vous avez demandé à la base de données pour un utilisateur avec un certain id, vous vous attendez à ce que le résultat soit d'un certain type (= user). Mais que se passe-t-il s'il n'y a pas d'utilisateur de cet identifiant? Une (mauvaise) solution consiste à: ajouter une nullvaleur. Le résultat est donc ambigu : soit c'est un, usersoit c'est ça null. Mais nulln'est pas le type attendu. Et c'est là que commence l' odeur du code .

En évitant null, votre code est sémantiquement clair.

Il y a toujours moyen de contourner le nullproblème: refactoriser les collections Null-objects, optionalsetc.

Thomas Junk
la source
-2

S'il est sémantiquement possible d'accéder à un emplacement de stockage de type référence ou pointeur avant l'exécution du code pour en calculer la valeur, il n'y a pas de choix parfait quant à ce qui devrait se passer. Avoir de tels emplacements par défaut des références de pointeur nulles qui peuvent être librement lues et copiées, mais qui présenteront une défaillance si elles sont déréférencées ou indexées, est un choix possible. Les autres choix incluent:

  1. Définissez les emplacements par défaut sur un pointeur null, mais n'autorisez pas le code à les consulter (ou même à déterminer qu'ils sont nuls) sans provoquer de crash. Fournissez éventuellement un moyen explicite de définir un pointeur sur null.
  2. Définit l'emplacement par défaut sur un pointeur null et se bloque en cas de tentative de lecture, mais fournit un moyen de vérifier si un pointeur contient la valeur null. Fournissez également un moyen explicite de définir un pointeur sur null.
  3. Les emplacements ont-ils comme valeur par défaut un pointeur sur une instance donnée par défaut fournie par le compilateur du type indiqué.
  4. Les emplacements ont-ils par défaut un pointeur sur une instance particulière fournie par programme du type indiqué.
  5. N'importe quel accès null-pointeur (pas seulement les opérations de déréférencement) appelle une routine fournie par le programme pour fournir une instance.
  6. Concevez la sémantique du langage de telle sorte que les ensembles d’emplacement de stockage n’existent pas, à l’exception d’un compilateur temporaire inaccessible, jusqu’à ce que les routines d’initialisation aient été exécutées sur tous les membres (un constructeur de tableau devrait être fourni avec une instance par défaut, une fonction qui renverrait une instance, ou éventuellement une paire de fonctions - une pour renvoyer une instance et une autre qui serait appelée sur des instances précédemment construites si une exception se produisait lors de la construction du tableau).

Le dernier choix pourrait avoir un certain intérêt, en particulier si un langage incluait à la fois des types nullable et non nullable (on pourrait appeler les constructeurs de tableaux spéciaux pour n’importe quel type, mais il ne serait nécessaire de les appeler que pour créer des tableaux de types non nullables), mais n'aurait probablement pas été réalisable à l'époque où des pointeurs nuls ont été inventés. Parmi les autres choix, aucun ne semble plus attrayant que de permettre la copie de pointeurs nuls, mais pas de déréférencement ou d’indexation. L’approche n ° 4 pourrait être une option pratique et devrait être relativement peu coûteuse à mettre en œuvre, mais elle ne devrait certainement pas être la seule option. Exiger que les pointeurs pointent par défaut sur un objet valide particulier est bien pire que d'avoir des pointeurs sur une valeur nulle pouvant être lue ou copiée mais non déréférencée ou indexée.

supercat
la source
-2

Oui, NULL est une conception terrible , dans un monde orienté objet. En un mot, l'utilisation de NULL conduit à:

  • traitement des erreurs ad-hoc (au lieu d'exceptions)
  • sémantique ambiguë
  • lent au lieu d'échec rapide
  • pensée informatique vs pensée d'objet
  • objets mutables et incomplets

Consultez ce billet de blog pour une explication détaillée: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

yegor256
la source