Si null est mauvais, pourquoi les langages modernes l'implémentent-ils? [fermé]


82

Je suis sûr que les concepteurs de langages tels que Java ou C # connaissaient des problèmes liés à l'existence de références null (voir Est-ce que les références null sont vraiment une mauvaise chose? ). De plus, implémenter un type d'option n'est pas vraiment beaucoup plus complexe que les références nulles.

Pourquoi ont-ils décidé de l'inclure de toute façon? Je suis certain que le manque de références nulles encouragerait (ou même forcerait) un code de meilleure qualité (en particulier une meilleure conception de bibliothèque) à la fois des créateurs de langage et des utilisateurs.

Est-ce simplement à cause du conservatisme - "d'autres langues l'ont, nous devons l'avoir aussi ..."?


99
null is great. Je l'aime et l'utilise tous les jours.
Pieter B

17
@PieterB Mais l'utilisez-vous pour la majorité des références ou voulez-vous que la plupart des références ne soient pas nulles? L'argument n'est pas qu'il ne devrait pas y avoir de données nullable, mais seulement qu'il devrait être explicite et opt-in.

11
@PieterB Mais quand la majorité ne devrait pas être nullable, ne serait-il pas logique de faire de null-capacité l'exception plutôt que la valeur par défaut? Notez que, bien que la conception habituelle des types d’options consiste à forcer la vérification explicite des absences et du décompactage, on peut également utiliser la sémantique bien connue de Java / C # / ... pour les références null-opt-in (utiliser comme si ce n’était pas nullable, si nul). Cela éviterait au moins certains bugs et ferait une analyse statique beaucoup plus pratique qui se plaint de l'absence de contrôle nul.

20
WTF est avec vous les gars? Essayer de déréférencer une valeur nulle n’est pas un problème. Il génère TOUJOURS un AV / segfault et est donc corrigé. Y a-t-il une telle pénurie de bogues que vous devez vous en préoccuper? Si tel est le cas, j’en ai beaucoup, et aucun d’eux n’invoque les problèmes de références / pointeurs nuls.
Martin James

13
@MartinJames "Il génère TOUJOURS un AV / segfault et est donc réparé" - non, non, ce n'est pas le cas.
Détly

Réponses:


97

Déni de responsabilité: Étant donné que je ne connais personnellement aucun concepteur de langage, toute réponse que je vous donnerai sera spéculative.

De Tony Hoare lui-même:

Je l'appelle mon erreur d'un milliard de dollars. C’était l’invention de la référence null en 1965. À cette époque, je concevais le premier système de types complet pour les références dans un langage orienté objet (ALGOL W). Mon objectif était de veiller à ce que toute utilisation des références soit absolument sûre, avec une vérification automatique par le compilateur. Mais je ne pouvais pas résister à la tentation de mettre une référence nulle, tout simplement parce que c'était si facile à mettre en œuvre. Cela a entraîné d'innombrables erreurs, vulnérabilités et pannes système, qui ont probablement causé des millions de dollars de douleur et de dommages au cours des quarante dernières années.

L'accent est à moi.

Naturellement, cela ne lui semblait pas une mauvaise idée à l'époque. Il est probable que cela ait été en partie perpétué pour la même raison: si cela semblait être une bonne idée pour l'inventeur du tri rapide récompensé par le prix Turing, il n'est pas étonnant que beaucoup de gens ne comprennent toujours pas pourquoi c'est mal. Cela s'explique probablement aussi par le fait qu'il est pratique que les nouvelles langues soient similaires aux anciennes, à la fois pour des raisons de marketing et d'apprentissage. Exemple:

"Nous recherchions les programmeurs C ++. Nous avons réussi à en faire glisser beaucoup à mi-chemin de Lisp." -Guy Steele, co-auteur de la spécification Java

(Source: http://www.paulgraham.com/icad.html )

Et, bien sûr, C ++ a la valeur null car C a la valeur null et il n'est pas nécessaire d'entrer dans l'impact historique de C. Le type C # de J ++ remplacé, qui était l'implémentation de Java par Microsoft, et il a également remplacé le C ++ en tant que langage de choix pour le développement Windows, de sorte qu'il aurait pu l'obtenir de l'un ou l'autre.

EDIT Voici une autre citation de Hoare à considérer:

Les langages de programmation sont dans l’ensemble beaucoup plus compliqués qu’ils ne l’étaient: orientation objet, héritage et autres fonctionnalités n’ont pas encore été vraiment réfléchies du point de vue d’une discipline cohérente et scientifiquement fondée ou d’une théorie de la correction correcte . Mon postulat initial, que j'ai suivi toute ma vie en tant que scientifique, est que l'on utilise les critères de l'exactitude comme moyen de converger vers une conception de langage de programmation décent, qui ne crée pas de piège pour ses utilisateurs, et lequel les différents composants du programme correspondent clairement aux différents composants de sa spécification, vous pouvez donc raisonner en composition. [...] Les outils, y compris le compilateur, doivent être basés sur une théorie de ce que signifie écrire un programme correct. Entretien avec l'histoire orale de Philip L. Frana, 17 juillet 2002, Cambridge, Angleterre; Institut Charles Babbage, Université du Minnesota. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

Encore une fois, soulignez le mien. Sun / Oracle et Microsoft sont des entreprises et l’argent est au cœur de toute entreprise. Les avantages pour eux d’avoir nullpu l'emporter sur les inconvénients, ou bien ils ont peut-être eu une échéance trop serrée pour examiner pleinement la question. Voici un exemple d'erreur de langage différente qui s'est probablement produite à cause des délais:

C'est dommage que Cloneable soit brisé, mais cela arrive. Les API Java d'origine ont été réalisées très rapidement dans des délais très courts pour pouvoir respecter une fenêtre de marché proche. L'équipe Java originale a fait un travail incroyable, mais toutes les API ne sont pas parfaites. Clonable est un point faible et je pense que les gens devraient être conscients de ses limites. -Josh Bloch

(Source: http://www.artima.com/intv/bloch13.html )


32
Cher visiteur, comment puis-je améliorer ma réponse?
Doval

6
Vous n'avez pas réellement répondu à la question; vous avez seulement fourni des citations sur des opinions après coup et des gestes supplémentaires sur le «coût». (Si null est une erreur d'un milliard de dollars, les économies
réalisées

29
@DougM Qu'attendez-vous de moi? Consultez chaque concepteur de langage des 50 dernières années et demandez-lui pourquoi il a implémenté nullson langage. Toute réponse à cette question sera spéculative sauf si elle provient d’un concepteur de langage. Je ne connais personne qui fréquente ce site à part Eric Lippert. La dernière partie est un hareng rouge pour de nombreuses raisons. La quantité de code tiers écrit au-dessus des API de MS et de Java dépasse évidemment la quantité de code contenue dans l'API elle-même. Donc, si vos clients veulent null, vous leur donnez null. Vous supposez également qu'ils ont accepté nullleur coûte de l'argent.
Doval

3
Si la seule réponse que vous pouvez donner est spéculative, indiquez-le clairement dans votre premier paragraphe. (Vous avez demandé comment vous pourriez améliorer votre réponse et j'ai répondu. Toute parenthèse n'est qu'un commentaire que vous pouvez vous sentir libre d'ignorer; c'est à quoi servent les parenthèses en anglais, après tout.)
DougM

7
Cette réponse est raisonnable. J'ai ajouté quelques considérations supplémentaires dans la mienne. Je note que ICloneableest pareillement cassé dans .NET; Malheureusement, c'est un endroit où les lacunes de Java n'ont pas été apprises à temps.
Eric Lippert

121

Je suis sûr que les concepteurs de langages comme Java ou C # connaissaient l'existence de références nulles.

Bien sûr.

De plus, implémenter un type d'option n'est pas vraiment beaucoup plus complexe que les références nulles.

Je ne suis pas d'accord! Les considérations de conception incluses dans les types de valeur nullable en C # 2 étaient complexes, controversées et difficiles. Ils ont pris les équipes de conception des langues et de l'exécution plusieurs mois de débat, de mise en œuvre de prototypes, etc., et en fait, la sémantique de la boxe annulable a été modifiée de manière très proche de l'envoi du C # 2.0, ce qui était très controversé.

Pourquoi ont-ils décidé de l'inclure de toute façon?

Toute conception est un processus de choix parmi de nombreux objectifs incompatibles de manière subtile et flagrante; Je ne peux donner qu'un bref aperçu de quelques-uns des facteurs à prendre en compte:

  • L'orthogonalité des caractéristiques linguistiques est généralement considérée comme une bonne chose. C # a des types de valeur nullables, des types de valeur non nullables et des types de référence nullables. Les types de référence non nulles n'existent pas, ce qui rend le système de types non orthogonal.

  • La connaissance des utilisateurs existants de C, C ++ et Java est importante.

  • Une interopérabilité facile avec COM est importante.

  • Une interopérabilité facile avec tous les autres langages .NET est importante.

  • Une interopérabilité facile avec les bases de données est importante.

  • La cohérence de la sémantique est importante. si nous avons la référence TheKingOfFrance égale à null, cela signifie-t-il toujours "il n'y a pas de roi de France à l'heure actuelle", ou peut-il aussi signifier "Il existe bel et bien un roi de France; je ne sais tout simplement pas de qui il s'agit maintenant"? ou peut-il vouloir dire "l'idée même d'avoir un roi en France est absurde, alors ne posez même pas la question!"? Null peut signifier toutes ces choses et plus encore en C #, et tous ces concepts sont utiles.

  • Le coût de performance est important.

  • Être disposé à une analyse statique est important.

  • La cohérence du système de types est importante; pouvons - nous toujours savoir qui est une référence non annulable jamais en aucun cas observés invalide? Qu'en est-il dans le constructeur d'un objet avec un champ de type référence non nullable? Qu'en est-il dans le finaliseur d'un tel objet, où l'objet est finalisé parce que le code qui était censé remplir la référence renvoyait une exception ? Un système de type qui vous ment sur ses garanties est dangereux.

  • Et que dire de la cohérence de la sémantique? Les valeurs nulles se propagent lorsqu'elles sont utilisées, mais les références nulles émettent des exceptions lorsqu'elles sont utilisées. C'est incohérent Cette incohérence est-elle justifiée par un avantage?

  • Pouvons-nous implémenter la fonctionnalité sans en casser d'autres? Quelles autres fonctionnalités futures possibles la fonctionnalité exclut-elle?

  • Vous allez faire la guerre à l'armée que vous avez, pas à celle que vous souhaitez. N'oubliez pas que C # 1.0 n'avait pas de génériques. Par conséquent, le fait de parler Maybe<T>d'alternative est un non-débutant. .NET aurait-il dû glisser depuis deux ans pendant que l'équipe d'exécution ajoutait des génériques, uniquement pour éliminer les références nulles?

  • Qu'en est-il de la cohérence du système de types? Vous pouvez dire Nullable<T>pour n'importe quel type de valeur - non, attendez, c'est un mensonge. Tu ne peux pas dire Nullable<Nullable<T>>. Devriez-vous pouvoir? Si oui, quelle est la sémantique souhaitée? Vaut-il la peine de donner à tout le système de types un cas particulier juste pour cette fonctionnalité?

Etc. Ces décisions sont complexes.


12
+1 pour tout mais surtout pour les génériques. Il est facile d'oublier qu'il y a eu des périodes dans l'histoire de Java et de C # où les génériques n'existaient pas.
Doval

2
Peut-être une question idiote (je ne suis qu'un étudiant en informatique) - mais le type d'option ne pourrait pas être implémenté au niveau syntaxique (avec CLR ne sachant rien à ce sujet) en tant que référence nullable régulière nécessitant une vérification "has-value" avant d'être utilisé dans code? Je crois que les types d'options n'ont pas besoin de contrôles au moment de l'exécution.
mrpyo

2
@ mrpyo: Bien sûr, c'est un choix d'implémentation possible. Aucun des autres choix de conception ne disparaît et ce choix de mise en œuvre présente de nombreux avantages et inconvénients.
Eric Lippert

1
@ mrpyo Je pense que forcer une vérification "has-value" n'est pas une bonne idée. Théoriquement, c’est une très bonne idée, mais dans la pratique, IMO apporterait toutes sortes de vérifications vides, juste pour satisfaire le compilateur - comme les exceptions vérifiées en Java et que les gens qui le trompent ne catchesfont rien. Je pense qu'il est préférable de laisser le système exploser au lieu de continuer à fonctionner dans un état éventuellement non valide.
NothingsImpossible

2
@voo: Les tableaux de types non référencés sont difficiles pour beaucoup de raisons. Il existe de nombreuses solutions possibles qui impliquent toutes des coûts pour différentes opérations. Supercat suggère de vérifier si un élément peut être lu légalement avant son attribution, ce qui impose des coûts. Vous devez vous assurer qu'un initialiseur est exécuté sur chaque élément avant que le tableau ne soit visible, ce qui impose un ensemble de coûts différent. Alors voici le problème: peu importe laquelle de ces techniques on choisit, quelqu'un va se plaindre que ce n'est pas efficace pour son scénario animal de compagnie. Ce sont des points sérieux contre la fonctionnalité.
Eric Lippert

28

Null sert un objectif très valable de représenter un manque de valeur.

Je dirai que je suis la personne la plus éloquente que je connaisse au sujet des abus de null et de tous les maux de tête et de souffrances qu’ils peuvent causer, en particulier lorsqu’ils sont utilisés généreusement.

Ma position personnelle est que les personnes peuvent utiliser des valeurs nulles uniquement lorsqu'elles peuvent justifier qu'elles sont nécessaires et appropriées.

Exemple justifiant des valeurs nulles:

Date de décès est généralement un champ nullable. Il y a trois situations possibles avec la date du décès. Soit la personne est décédée et la date est connue, la personne est décédée et la date est inconnue, soit la personne n’est pas morte et, par conséquent, la date du décès n’existe pas.

Date of Death est également un champ DateTime et n'a pas de valeur "unknown" ou "empty". La date par défaut qui apparaît lorsque vous créez une nouvelle date / heure varie en fonction de la langue utilisée, mais il est techniquement possible que cette personne décède à ce moment-là et qu'elle soit considérée comme votre "valeur vide" si vous deviez utilisez la date par défaut.

Les données devraient représenter la situation correctement.

La personne est morte la date du décès est connue (3/9/1984)

Simple, '3/9/1984'

La personne est morte date inconnue

Alors quoi de mieux? Null , '0/0/0000' ou '01 / 01/1869 '(ou quelle que soit votre valeur par défaut est?)

Personne n'est pas la date morte du décès n'est pas applicable

Alors quoi de mieux? Null , '0/0/0000' ou '01 / 01/1869 '(ou quelle que soit votre valeur par défaut est?)

Alors réfléchissons à chaque valeur ...

  • Null , cela a des implications et des préoccupations dont vous devez vous méfier, essayer accidentellement de le manipuler sans confirmer que ce n'est pas nul d'abord, par exemple, jetterait une exception, mais cela représenterait également le mieux la situation réelle ... Si la personne n'est pas morte la date du décès n'existe pas ... ce n'est rien ... c'est nul ...
  • 0/0/0000 , Cela pourrait bien se passer dans certaines langues et pourrait même constituer une représentation appropriée sans date. Malheureusement, certaines langues et validations rejetteront cela comme une date / heure invalide, ce qui en fait un non-lieu dans de nombreux cas.
  • 1/1/1869 (ou quelle que soit votre valeur date / heure par défaut) , le problème est qu'il devient difficile à gérer. Vous pouvez utiliser cela comme valeur de valeur nulle, sauf ce qui se passe si je veux filtrer tous mes dossiers pour lesquels je n'ai pas de date de décès? Je pourrais facilement filtrer les personnes décédées à cette date, ce qui pourrait causer des problèmes d'intégrité des données.

Le fait est parfois vous ne doivent représenter rien et que parfois un type de variable fonctionne bien pour cela, mais les types souvent variables doivent être en mesure de représenter rien.

Si je n'ai pas de pommes, j'ai 0 pommes, mais que se passe-t-il si je ne sais pas combien de pommes j'ai?

Bien entendu, null est utilisé de manière abusive et potentiellement dangereux, mais il est parfois nécessaire. Ce n'est que par défaut dans de nombreux cas car jusqu'à ce que je fournisse une valeur, l'absence d'une valeur et quelque chose doit la représenter. (Nul)


37
Null serves a very valid purpose of representing a lack of value.Un Optionou Maybetype sert ce but très valable sans contourner le système de types.
Doval

34
Personne ne prétend qu'il ne devrait pas y avoir de valeur manquant de valeur, mais que les valeurs éventuellement manquantes devraient être explicitement désignées comme telles, plutôt que chaque valeur potentiellement manquante.

2
Je suppose que RualStorge parlait de SQL, car il existe des camps qui indiquent que chaque colonne doit être marquée comme NOT NULL. Ma question n'était cependant pas liée au SGBDR ...
mrpyo

5
+1 pour faire la distinction entre "aucune valeur" et "valeur inconnue"
David,

2
Ne serait-il pas plus logique de séparer l'état d'une personne? Ie Un Persontype a un statechamp de type State, qui est une union discriminée de Aliveet Dead(dateOfDeath : Date).
jon-hanson

10

Je n'irais pas aussi loin que "d'autres langues l'ont, nous devons l'avoir aussi ...", comme si c'était comme suivre les Jones. Une caractéristique clé de tout nouveau langage est la capacité d'interopérer avec des bibliothèques existantes dans d'autres langages (lire: C). Puisque C a des pointeurs nuls, la couche d'interopérabilité a nécessairement besoin du concept de zéro (ou de tout autre équivalent "n'existe pas" qui explose lorsque vous l'utilisez).

Le concepteur de langage aurait pu choisir d’utiliser les types d’option et de vous obliger à gérer le chemin nul où les choses puissent être nulles. Et cela conduirait presque certainement à moins de bugs.

Mais (surtout pour Java et C # en raison du moment choisi pour leur introduction et de leur public cible), l’utilisation des types d’options pour cette couche d’interopérabilité aurait probablement nui, voire torpillé, leur adoption. Soit le type d’option est transmis jusqu’à la fin, gênant énormément les programmeurs C ++ du milieu à la fin des années 90 - ou la couche d’interopérabilité jetterait des exceptions lorsqu’elle rencontrait des valeurs nulles, énervant énormément les programmeurs C ++ du milieu à la fin des années 1990. ..


3
Le premier paragraphe n'a pas de sens pour moi. Java n’a pas l’interopérabilité C dans la forme que vous suggérez (il existe JNI mais elle saute déjà à travers une douzaine de cercles pour tout ce qui concerne les références; de plus, elle est rarement utilisée dans la pratique), de même que pour les autres langages "modernes".

@delnan - désolé, je suis plus familier avec C #, qui a ce type d'interopérabilité. J'ai plutôt supposé que de nombreuses bibliothèques Java fondamentales utilisaient également JNI en bas.
Telastyn

6
Vous faites un bon argument pour autoriser null, mais vous pouvez toujours autoriser null sans l' encourager . Scala en est un bon exemple. Il peut interagir de manière transparente avec les apis Java utilisant null, mais il est conseillé de l’envelopper Optiondans Scala, ce qui est aussi simple que val x = Option(possiblyNullReference). En pratique, il ne faut pas longtemps pour que les gens voient les avantages d’un Option.
Karl Bielefeldt

1
Les types d'option vont de pair avec la correspondance de motifs (vérifiée statiquement), ce que C # n'a malheureusement pas. F # fait cependant, et c'est merveilleux.
Steven Evers

1
@SteveEvers Il est possible de le simuler en utilisant une classe de base abstraite avec constructeur privé, des classes internes scellées et une Matchméthode qui prend les délégués en argument. Ensuite, vous transmettez les expressions lambda à Match(points de bonus pour l’utilisation d’arguments nommés) et Matchappelez celle de droite.
Doval

7

Tout d'abord, je pense que nous pouvons tous convenir qu'un concept de nullité est nécessaire. Il y a des situations où nous devons représenter l' absence d'information.

Autoriser les nullréférences (et les pointeurs) n’est qu’une des implémentations de ce concept, et peut-être même le plus populaire, bien qu’il soit connu pour avoir des problèmes: C, Java, Python, Ruby, PHP, JavaScript,… utilisent tous le même null.

Pourquoi ? Eh bien, quelle est l'alternative?

Dans les langages fonctionnels tels que Haskell, vous avez le type Optionou Maybe; cependant, ceux-ci sont construits sur:

  • types paramétriques
  • types de données algébriques

Les versions C, Java, Python, Ruby ou PHP d’origine prennent-elles en charge l’une ou l’autre de ces fonctionnalités? Les génériques défectueux de Java sont récents dans l'histoire du langage et je doute que les autres les mettent même en œuvre.

Voilà. nullest facile, les types de données algébriques paramétriques sont plus difficiles. Les gens ont opté pour la solution la plus simple.


+1 pour "null is easy, les types de données algébriques paramétriques sont plus difficiles." Mais je pense que ce n'était pas tellement un problème de dactylographie paramétrique et ADT étant plus difficile; c'est juste qu'ils ne sont pas perçus comme nécessaires. Si Java avait été livré sans système d’objets, en revanche, il se serait effondré; La programmation orientée objet était une fonctionnalité de démonstration, en ce sens que si vous ne l'aviez pas, personne ne serait intéressé.
Doval

@Doval: eh bien, la POO aurait pu être nécessaire pour Java, mais ce n'était pas pour C :) Mais il est vrai que Java visait à être simple. Malheureusement, les gens semblent penser qu'un langage simple mène à des programmes simples, ce qui est assez étrange (Brainfuck est un langage très simple ...), mais nous sommes certainement d'accord sur le fait que les langages compliqués (C ++ ...) ne sont pas non plus une panacée ils peuvent être incroyablement utiles.
Matthieu M.

1
@MatthieuM .: Les systèmes réels sont complexes. Un langage bien conçu dont les complexités correspondent au système du monde réel modélisé peut permettre de modéliser le système complexe avec un code simple. Les tentatives visant à simplifier à outrance une langue imposent simplement la complexité au programmeur qui l’utilise.
Supercat

@supercat: je suis tout à fait d'accord. Ou comme le dit Einstein: "Faites en sorte que tout soit aussi simple que possible, mais pas plus simple."
Matthieu M.

@MatthieuM .: Einstein était sage à bien des égards. Les langages qui essaient d'assumer "tout est un objet, une référence à laquelle on peut stocker Object" ne reconnaissent pas que les applications pratiques ont besoin d'objets mutables non partagés et d'objets immuables partageables (qui doivent tous deux se comporter comme des valeurs), ainsi que d'objets partageables et non échangeables. entités. L'utilisation d'un seul Objecttype pour tout n'élimine pas le besoin de telles distinctions; cela rend simplement plus difficile de les utiliser correctement.
Supercat

5

Null / nil / none n'est pas mauvais en soi.

Si vous regardez son célèbre discours trompeur "L'erreur d'un milliard de dollars", Tony Hoare explique à quel point le fait d'autoriser une variable à conserver la valeur null était une énorme erreur. L'alternative - à l' aide des options - ne pas en fait se débarrasser des références nulles. Au lieu de cela, il vous permet de spécifier quelles variables sont autorisées à contenir null et celles qui ne le sont pas.

En fait, avec les langages modernes qui implémentent le traitement des exceptions, les erreurs de déréférencement nul ne sont pas différentes des autres exceptions: vous les trouvez, vous les corrigez. Certaines alternatives aux références nulles (le modèle Null Object, par exemple) cachent les erreurs, ce qui entraîne l'échec silencieux des choses jusque beaucoup plus tard. À mon avis, il vaut beaucoup mieux échouer rapidement .

La question est donc de savoir pourquoi les langues ne parviennent pas à implémenter Options. En fait, le langage sans doute le plus populaire de tous les temps, le C ++, est capable de définir des variables d'objet qui ne peuvent pas être attribuées NULL. C’est une solution au "problème nul" que Tony Hoare a mentionné dans son discours. Pourquoi le prochain langage typé le plus populaire, Java, ne l’a-t-il pas? On pourrait se demander pourquoi il a tant de défauts en général, en particulier dans son système de types. Je ne pense pas que vous puissiez vraiment dire que les langues font systématiquement cette erreur. Certains le font, d'autres pas.


1
L'une des plus grandes forces de Java du point de vue de la mise en œuvre, mais des faiblesses du point de vue du langage, est qu'il n'existe qu'un seul type non primitif: la référence d'objet Promiscuous. Cela simplifie énormément l'exécution, rendant possibles des implémentations extrêmement légères de JVM. Cette conception, cependant, signifie que chaque type doit avoir une valeur par défaut et que, pour une référence d'objet Promiscuous, la seule valeur par défaut possible est null.
Supercat

Eh bien, un type de racine non primitif en tout cas. Pourquoi est-ce une faiblesse du point de vue linguistique? Je ne comprends pas pourquoi cela exige que chaque type ait une valeur par défaut (ou inversement pourquoi plusieurs types de racine permettraient aux types de ne pas avoir de valeur par défaut), ni pourquoi c'est une faiblesse.
BT

Quel autre type de non-primitif un élément de champ ou de tableau peut-il contenir? La faiblesse est que certaines références sont utilisées pour encapsuler une identité et d’autres pour encapsuler les valeurs contenues dans les objets identifiés par celle-ci. Pour les variables de type référence utilisées pour encapsuler une identité, il nulls'agit du seul paramètre par défaut raisonnable. Cependant, les références utilisées pour encapsuler une valeur pourraient avoir un comportement par défaut raisonnable dans les cas où un type aurait ou pourrait construire une instance par défaut raisonnable. De nombreux aspects du comportement des références dépendent de savoir si et comment elles encapsulent de la valeur, mais ...
Supercat

... le système de types Java n'a aucun moyen d'exprimer cela. Si foodétient la seule référence à un int[]conteneur {1,2,3}et que le code souhaite fooconserver une référence à un int[]conteneur {2,2,3}, le moyen le plus rapide d'y parvenir serait d'incrémenter foo[0]. Si le code veut faire savoir à une méthode qu'elle footient {1,2,3}, l'autre méthode ne modifiera pas le tableau, ni ne persistera une référence au-delà du point où vous foovoudriez le modifier, le moyen le plus rapide d'y parvenir serait de passer une référence au tableau. Si Java avait un type de "référence éphémère en lecture seule", alors ...
Supercat

... le tableau peut être passé en toute sécurité comme référence éphémère, et une méthode qui souhaite conserver sa valeur saura qu'elle doit le copier. En l'absence d'un tel type, le seul moyen d'exposer en toute sécurité le contenu d'un tableau consiste à en faire une copie ou à l'encapsuler dans un objet créé uniquement à cette fin.
Supercat

4

Parce que les langages de programmation sont généralement conçus pour être pratiques plutôt que techniquement corrects. Le fait est que les nullétats sont fréquents en raison de données incorrectes ou manquantes ou d'un état qui n'a pas encore été décidé. Les solutions techniquement supérieures sont toutes plus lourdes que de simplement autoriser les états nuls et d’absorber le fait que les programmeurs font des erreurs.

Par exemple, si je veux écrire un script simple qui fonctionne avec un fichier, je peux écrire un pseudo-code comme:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

et cela échouera simplement si joebloggs.txt n'existe pas. Le problème, c’est que, pour les scripts simples, c’est probablement correct et que, dans de nombreuses situations de code plus complexe, je sais qu’il existe et que l’échec ne se produira pas. Me forcer à vérifier me fait perdre mon temps. Les alternatives plus sûres atteignent leur sécurité en m'obligeant à gérer correctement l'état de défaillance potentiel, mais souvent je ne veux pas le faire, je veux juste continuer.


13
Et ici, vous avez donné un exemple de ce qui ne va pas avec les valeurs nulles. La fonction "openfile" correctement implémentée devrait générer une exception (pour le fichier manquant) qui arrêterait l'exécution à ce stade avec une explication exacte de ce qui s'est passé. Au lieu de cela, s'il renvoie null, il se propage plus loin (à for line in file) et lève une exception de référence null sans signification, ce qui est acceptable pour un programme aussi simple, mais pose de véritables problèmes de débogage dans des systèmes beaucoup plus complexes. Si la valeur null n'existait pas, le concepteur "openfile" ne pourrait pas commettre cette erreur.
mrpyo

2
+1 pour "Parce que les langages de programmation sont généralement conçus pour être pratiques plutôt que techniquement corrects"
Martin Ba,

2
Chaque type d'option que je connais vous permet d'effectuer l'échec-sur-null avec un seul appel de méthode extra court (exemple Rust:) let file = something(...).unwrap(). En fonction de votre POV, c’est un moyen simple de ne pas gérer les erreurs ou les assertions succinctes qui ne peuvent pas être nulles. Le temps perdu est minime et vous gagnez du temps ailleurs car vous n'avez pas à déterminer si quelque chose peut être nul. Un autre avantage (qui peut à lui seul valoir l'appel supplémentaire) est que vous ignorez explicitement le cas d'erreur. quand il échoue, il ne fait aucun doute que ce qui a mal tourné et où le correctif doit aller.

4
@mrpyo Toutes les langues ne prennent pas en charge les exceptions et / ou la gestion des exceptions (à la tentative / interceptées). Et les exceptions peuvent aussi être abusées - "exception comme contrôle de flux" est un anti-modèle courant. Ce scénario - un fichier n'existe pas - est, autant que je sache, l'exemple le plus souvent cité de cet anti-motif. Il semblerait que vous remplaciez une mauvaise pratique par une autre.
David

8
@mrpyo if file exists { open file }souffre d'une situation de compétition . Le seul moyen fiable de savoir si l'ouverture d'un fichier réussira est d'essayer de l'ouvrir.

4

Il existe des utilisations claires et pratiques du pointeur NULL(ou nil, ou Nil, ou null, Nothingou de quelque manière que ce soit appelé dans votre langue préférée).

Pour les langues qui ne disposent pas d'un système d'exception (par exemple C), un pointeur NULL peut être utilisé comme marque d'erreur lorsqu'un pointeur doit être renvoyé. Par exemple:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Ici, un NULLretour de malloc(3)est utilisé comme marqueur d'échec.

Lorsqu'il est utilisé dans les arguments de méthode / fonction, il peut indiquer use default pour l'argument ou ignorer l'argument de sortie. Exemple ci-dessous.

Même pour les langues avec mécanisme d'exception, un pointeur null peut être utilisé comme indication d'erreur logicielle (c'est-à-dire d'erreurs récupérables), en particulier lorsque la gestion des exceptions est coûteuse (par exemple, Objective-C):

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

Ici, l'erreur logicielle ne provoque pas le blocage du programme s'il n'est pas intercepté. Ceci élimine le try-catch fou comme Java et permet de mieux contrôler le déroulement du programme, car les erreurs logicielles ne s'interrompent pas (et les quelques exceptions matérielles restantes ne sont généralement pas récupérables et ne sont pas récupérées)


5
Le problème est qu’il n’ya aucun moyen de distinguer les variables qui ne devraient jamais contenir nullde celles qui devraient. Par exemple, si je veux un nouveau type contenant 5 valeurs en Java, je pourrais utiliser une énumération, mais ce que je reçois est un type pouvant contenir 6 valeurs (les 5 que je voulais + null). C'est une faille dans le système de types.
Doval

@Doval Si c'est le cas, attribuez simplement une signification à NULL (ou si vous avez une valeur par défaut, considérez-la comme un synonyme de la valeur par défaut) ou utilisez la valeur NULL (qui ne devrait jamais apparaître à la première place) comme marqueur d'erreur logicielle. (c.-à-d. erreur mais au moins pas encore écrasé)
Maxthon Chan

1
@MaxtonChan Nullne peut se voir attribuer une signification que lorsque les valeurs d'un type ne contiennent aucune donnée (par exemple, des valeurs enum). Dès que vos valeurs sont plus compliquées (par exemple, une structure), nullvous ne pouvez attribuer une signification qui ait du sens pour ce type. Il n'y a aucun moyen d'utiliser un nullcomme structure ou une liste. Et, encore une fois, le problème de l’utilisation en nulltant que signal d’erreur est que nous ne pouvons pas déterminer ce qui peut renvoyer null ni accepter null. N'importe quelle variable de votre programme pourrait être, à nullmoins que vous ne deveniez extrêmement méticuleux, de vérifier chacune d'entre elles nullavant chaque utilisation, ce que personne ne fait.
Doval

1
@Doval: Il n'y aurait aucune difficulté inhérente particulière à avoir un type de référence immuable considéré nullcomme une valeur par défaut utilisable (par exemple, avoir la valeur par défaut de stringse comporter comme une chaîne vide, comme c'était le cas dans le précédent modèle à objets communs). Tout ce qui aurait été nécessaire aurait été d'utiliser les langues callplutôt que d' callvirtappeler des membres non virtuels.
Supercat

@supercat C'est un bon point, mais maintenant n'avez-vous pas besoin d'ajouter un support pour distinguer les types immuables des types immuables? Je ne sais pas à quel point il est trivial d’ajouter à une langue.
Doval

4

Il y a deux problèmes liés, mais légèrement différents:

  1. Devrait nullexister du tout? Ou devriez-vous toujours utiliser Maybe<T>où null est utile?
  2. Toutes les références doivent-elles être nulles? Si non, quelle devrait être la valeur par défaut?

    Avoir à déclarer explicitement les types de référence nullable comme string?ou similaires éviterait la plupart des problèmes (mais pas tous) null, sans être trop différent de ce à quoi les programmeurs sont habitués.

Je suis au moins d’accord avec vous pour dire que toutes les références ne devraient pas pouvoir être annulées. Mais éviter la nullité n’est pas sans complexité:

.NET initialise tous les champs default<T>avant d’être accessibles en premier par le code managé. Cela signifie que pour les types de référence dont vous avez besoin nullou quelque chose d'équivalent, les types de valeur peuvent être initialisés à une sorte de zéro sans exécuter de code. Bien que ces deux solutions présentent des inconvénients graves, la simplicité d’ defaultinitialisation a peut-être dépassé ces inconvénients.

  • Par exemple, vous pouvez contourner ce problème en imposant l'initialisation des champs avant d'exposer le thispointeur au code géré. Spec # a choisi cette voie en utilisant une syntaxe différente de celle du constructeur par rapport à C #.

  • Cela est plus difficile pour les champs statiques , à moins que vous ne posiez de fortes restrictions sur le type de code pouvant s'exécuter dans un initialiseur de champ, car vous ne pouvez pas simplement masquer le thispointeur.

  • Comment initialiser des tableaux de types de référence? Considérez un List<T>qui est soutenu par un tableau avec une capacité supérieure à la longueur. Les éléments restants doivent avoir une certaine valeur.

Un autre problème est qu'il n'autorise pas les méthodes telles bool TryGetValue<T>(key, out T value)que return, default(T)comme valuesi elles ne trouvaient rien. Bien que, dans ce cas, il soit facile de soutenir que le paramètre out est une mauvaise conception en premier lieu et que cette méthode devrait renvoyer une union discriminante ou peut - être à la place.

Tous ces problèmes peuvent être résolus, mais ce n'est pas aussi simple que "interdire la nullité et tout va bien".


Le List<T>meilleur exemple est l’IMHO, car il faudrait soit que chaque Télément ait une valeur par défaut, que chaque élément du magasin de stockage secondaire soit Maybe<T>associé à un champ "isValid" supplémentaire, même lorsque Test un Maybe<U>, ou que le code correspondant List<T>se comporte différemment selon sur si Test lui-même un type nullable. Je considérerais que l'initialisation des T[]éléments sur une valeur par défaut est le moindre mal de ces choix, mais cela signifie bien sûr que les éléments doivent avoir une valeur par défaut.
Supercat

La rouille suit le point 1 - pas de null du tout. Ceylan suit le point 2 - non nul par défaut. Les références pouvant être null sont explicitement déclarées avec un type d'union incluant une référence ou null, mais null ne peut jamais être la valeur d'une référence simple. En conséquence, le langage est totalement sûr et il n'y a pas d'exception NullPointerException car ce n'est pas sémantiquement possible.
Jim Balter

2

Les langages de programmation les plus utiles permettent d’écrire et de lire des éléments de données en séquences arbitraires, de sorte qu’il sera souvent impossible de déterminer de manière statique l’ordre dans lequel les lectures et les écritures auront lieu avant l’exécution d’un programme. Il existe de nombreux cas où le code stocke effectivement des données utiles dans chaque emplacement avant de le lire, mais il est difficile de le prouver. Ainsi, il sera souvent nécessaire d'exécuter des programmes où il serait au moins théoriquement possible au code d'essayer de lire quelque chose qui n'a pas encore été écrit avec une valeur utile. Qu'il soit légal ou non de le faire, il n'y a pas de moyen général d'empêcher le code de tenter le coup. La seule question est ce qui devrait se passer quand cela se produit.

Différents langages et systèmes adoptent des approches différentes.

  • Une approche serait de dire que toute tentative de lire quelque chose qui n'a pas été écrit déclenchera une erreur immédiate.

  • Une deuxième approche consiste à exiger que le code fournisse une valeur dans chaque emplacement avant de pouvoir le lire, même s’il n’y aurait aucun moyen que la valeur stockée soit sémantiquement utile.

  • Une troisième approche consiste simplement à ignorer le problème et à laisser ce qui arriverait "naturellement" simplement.

  • Une quatrième approche consiste à dire que chaque type doit avoir une valeur par défaut, et tout emplacement qui n’a pas été écrit avec quoi que ce soit d’autre aura par défaut cette valeur.

L'approche n ° 4 est beaucoup plus sûre que l'approche n ° 3 et est en général moins chère que les approches n ° 1 et n ° 2. Cela laisse alors la question de ce que la valeur par défaut devrait être pour un type de référence. Pour les types de référence immuables, il serait souvent judicieux de définir une instance par défaut et d'indiquer que la valeur par défaut pour toute variable de ce type doit être une référence à cette instance. Pour les types de référence mutables, cependant, cela ne serait pas très utile. Si vous tentez d'utiliser un type de référence mutable avant qu'il ne soit écrit, il n'y a généralement pas de solution sûre, sauf pour intercepter au point de tentative d'utilisation.

Sémantiquement, si l’on a un tableau customersde type Customer[20]et que l’on essaie Customer[4].GiveMoney(23)sans rien stocker dans l’ Customer[4]exécution, il va falloir exécuter. On pourrait argumenter qu'une tentative de lecture Customer[4]devrait intercepter immédiatement plutôt que d'attendre que le code le tente GiveMoney, mais il y a suffisamment de cas où il est utile de lire un emplacement, de découvrir qu'il ne contient pas de valeur, puis de l'utiliser. informations, que l'échec de la tentative de lecture serait souvent une nuisance majeure.

Certaines langues permettent de spécifier que certaines variables ne doivent jamais contenir null, et toute tentative de stockage d'une valeur null doit déclencher une interruption immédiate. C'est une fonctionnalité utile. En général, cependant, tout langage permettant aux programmeurs de créer des tableaux de références devra soit permettre la possibilité d’éléments de tableau nuls, soit obliger l’initialisation d’éléments de tableau à des données qui ne peuvent pas avoir de sens.


Ne serait - pas Maybe/ Optioncar si vous n'avez pas une valeur pour votre référence avec # 2 de type résoudre le problème, encore , mais aurez un à l'avenir, il vous suffit de stocker Nothingdans un Maybe <Ref type>?
Doval

@Doval: Non, cela ne résoudrait pas le problème - du moins, sans introduire de nouveau des références nulles. Un "rien" doit-il agir comme un membre du type? Si oui, lequel? Ou devrait-il jeter une exception? Dans quel cas, comment êtes-vous mieux lotis que de simplement utiliser nullcorrectement / judicieusement?
cHao

@Doval: Le type de support d'un List<T>a T[]ou d'un Maybe<T>? Qu'en est-il du type de support d'un List<Maybe<T>>?
Supercat

@supercat Je ne sais pas comment un type de support de Maybelogique pour Listcar Maybedétient une seule valeur. Vouliez-vous dire Maybe<T>[]?
Doval

@cHao Nothingne peut être assigné qu'à des valeurs de type Maybe, ce n'est donc pas tout à fait comme assigner null. Maybe<T>et Tsont deux types distincts.
Doval
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.