web-dev-qa-db-fra.com

Si null est mauvais, pourquoi les langues modernes l'implémentent-elles?

Je suis sûr que les concepteurs de langages comme Java ou C # connaissaient des problèmes liés à l'existence de références nulles (voir Les références nulles sont-elles vraiment une mauvaise chose? ). le 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 sûr 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 langues et des utilisateurs.

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

84
mrpyo

Avertissement: É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:

J'appelle cela mon erreur d'un milliard de dollars. C'était l'invention de la référence nulle en 1965. A cette époque, je concevais le premier système de type complet pour les références dans un langage orienté objet (ALGOL W). Mon objectif était de garantir que toute utilisation des références soit absolument sûre, la vérification étant effectuée automatiquement par le compilateur. Mais je n'ai pas pu résister à la tentation de mettre une référence nulle, simplement parce qu'elle était si facile à implémenter. Cela a conduit à d'innombrables erreurs, vulnérabilités et les plantages du système, qui ont probablement causé un milliard de dollars de douleur et de dommages au cours des quarante dernières années.

Je souligne.

Naturellement, cela ne lui semblait pas une mauvaise idée à l'époque. Il est probable que cela ait été perpétué en partie pour la même raison - si cela semblait être une bonne idée pour l'inventeur de quicksort primé au Turing Award, il n'est pas surprenant que beaucoup de gens encore ne comprennent pas pourquoi c'est mal. Cela est également probable en partie parce qu'il est pratique que les nouvelles langues soient similaires aux langues plus anciennes, à la fois pour des raisons de marketing et de courbe d'apprentissage. Exemple concret:

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

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

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

[~ # ~] modifier [~ # ~] Voici une autre citation de Hoare à considérer:

Les langages de programmation dans leur ensemble sont beaucoup plus compliqués qu'ils ne l'étaient: l'orientation des objets, l'héritage et d'autres fonctionnalités ne sont toujours pas vraiment réfléchis du point de vue d'une approche cohérente et scientifiquement une discipline bien fondée ou une théorie de l'exactitude. Mon postulat d'origine, que j'ai poursuivi en tant que scientifique toute ma vie, est que l'on utilise les critères de l'exactitude comme moyen de converger vers une conception décente du langage de programmation - un qui ne pose pas de pièges à ses utilisateurs, et ceux dans lesquels les différents composants du programme correspondent clairement aux différents composants de ses spécifications, de sorte que vous pouvez raisonner de manière compositionnelle à ce sujet. [...] Les outils, y compris le compilateur, doivent être basés sur une théorie de ce que signifie écrire un programme correct. Entretien sur l'histoire orale par 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, c'est moi qui souligne. Sun/Oracle et Microsoft sont des entreprises, et le résultat net de toute entreprise est l'argent. Les avantages pour eux d'avoir null peuvent avoir dépassé les inconvénients, ou ils ont peut-être simplement eu un délai trop serré pour examiner pleinement le problème. À titre d'exemple d'une erreur de langage différente qui s'est probablement produite en raison de délais:

C'est dommage que Cloneable soit cassé, mais ça arrive. Les API originales de Java ont été réalisées très rapidement dans un délai serré pour respecter une fenêtre de fermeture du marché. L'original Java l'équipe a fait un travail incroyable, mais toutes les API ne sont pas parfaites. Cloneable 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 )

97
Doval

Je suis sûr que les concepteurs de langages comme Java ou C # connaissaient des problèmes liés à l'existence de références nulles

Bien sûr.

L'implémentation d'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 qui entraient dans les types de valeurs nullables en C # 2 étaient complexes, controversées et difficiles. Ils ont pris les équipes de conception des langages et de l'exécution de nombreux 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 très très près de l'expédition 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 subtilement et grossièrement incompatibles; Je ne peux que donner un bref aperçu de quelques-uns des facteurs qui seraient pris 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 nullable, des types de valeur non nullable et des types de référence nullable. Les types de référence non nullables n'existent pas, ce qui rend le système de types non orthogonal.

  • Familiarité avec les utilisateurs existants de C, C++ et Java est important.

  • L'interopérabilité facile avec COM est importante.

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

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

  • La cohérence de la sémantique est importante; si nous avons une référence TheKingOfFrance égale à null, cela signifie-t-il toujours "il n'y a pas de roi de France en ce moment", ou cela peut-il aussi signifier "il y a définitivement un roi de France; je ne sais tout simplement pas qui c'est en ce moment"? ou cela peut-il signifier "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 en C #, et tous ces concepts sont utiles.

  • Le coût de performance est important.

  • Être réceptif à l'analyse statique est important.

  • La cohérence du système de type est importante; pouvons-nous toujours savoir qu'une référence non-nullable est jamais sous toutes circonstances jugées invalides? Qu'en est-il du 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é car le code qui était censé remplir la référence a levé une exception? Un système de type qui vous ment sur ses garanties est dangereux.

  • Et qu'en est-il de la cohérence de la sémantique? Null valeurs se propage lorsqu'il est utilisé, mais null références lève des exceptions lorsqu'il est utilisé. C'est incohérent; cette incohérence est-elle justifiée par un avantage?

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

  • Vous partez en guerre avec l'armée que vous avez, pas celle que vous aimeriez. Rappelez-vous, C # 1.0 n'avait pas de génériques, donc parler de Maybe<T> comme alternative est un non-démarreur complet. Est-ce que .NET aurait dû glisser pendant deux ans alors que l'équipe d'exécution a ajouté 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? Tu peux dire Nullable<T> pour tout type de valeur - non, attendez, c'est un mensonge. Vous ne pouvez pas dire Nullable<Nullable<T>>. Devriez-vous pouvoir? Si oui, quelle est sa sémantique souhaitée? Vaut-il la peine de faire en sorte que l'ensemble du système de caractères ait un cas particulier uniquement pour cette fonctionnalité?

Etc. Ces décisions sont complexes.

121
Eric Lippert

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

Je dirai que je suis la personne la plus virulente que je connaisse sur les abus de null et tous les maux de tête et les souffrances qu'ils peuvent causer, surtout lorsqu'ils sont utilisés généreusement.

Ma position personnelle est que les gens peuvent utiliser des valeurs nulles seulement quand ils peuvent justifier que c'est nécessaire et approprié.

Exemple justifiant les nulls:

La date de décès est généralement un champ annulable. Il existe trois situations possibles avec la date du décès. Soit la personne est décédée et la date est connue, soit la personne est décédée et la date est inconnue, soit la personne n'est pas décédée et il n'existe donc pas de date de décès.

Date of Death est également un champ DateTime et n'a pas de valeur "inconnue" ou "vide". Il a la date par défaut qui apparaît lorsque vous créez un nouveau datetime qui varie en fonction de la langue utilisée, mais il y a techniquement une chance que la personne soit effectivement décédée à ce moment-là et qu'elle serait signalée comme votre "valeur vide" si vous deviez utilisez la date par défaut.

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

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

Simple, '3/9/1984'

La personne est décédée, la date du décès est inconnue

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

La personne n'est pas décédée, la date de 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?)

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 qu'il n'est pas nul en premier, par exemple, lèverait une exception, mais cela représente aussi 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 être correct dans certaines langues, et pourrait même être une représentation appropriée d'aucune date. Malheureusement, certaines langues et la validation rejetteront cela comme une date/heure non valide, ce qui en fait un non-go dans de nombreux cas.
  • 1/1/1869 (ou quelle que soit votre valeur datetime par défaut) , le problème ici est qu'il devient difficile à gérer. Vous pouvez utiliser cela comme votre manque de valeur, sauf ce qui se passe si je veux filtrer tous mes enregistrements 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 entraîner des problèmes d'intégrité des données.

Le fait est parfois que vous Do ne devez rien représenter et bien sûr, parfois un type de variable fonctionne bien pour cela, mais souvent les types de variables doivent être capables de ne rien représenter.

Si je n'ai pas de pommes, j'ai 0 pommes, mais que faire si je ne sais pas combien de pommes j'ai?

Bien sûr, null est abusé et potentiellement dangereux, mais c'est parfois nécessaire. Ce n'est que la valeur 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)

28
RualStorge

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

Le concepteur de langage aurait pu choisir d'utiliser Types d'options et vous forcer à gérer le chemin nul partout que les choses pourraient être nulles . Et cela conduirait presque certainement à moins de bugs.

Mais (en particulier pour Java et C # en raison du moment de leur introduction et de leur public cible), l'utilisation de types d'options pour cette couche d'interopérabilité aurait probablement nui, sinon torpillé leur adoption. Soit le type d'option est passé tout le chemin, ennuyant l'enfer des programmeurs C++ du milieu à la fin des années 90 - ou la couche d'interopérabilité lèverait des exceptions lors de la rencontre de null, ennuyant l'enfer des programmeurs C++ du milieu à la fin des années 90 ...

10
Telastyn

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 absence d'informations.

Autoriser null références (et pointeurs) n'est qu'une implémentation de ce concept, et peut-être le plus populaire bien qu'il soit connu d'avoir des problèmes: C, Java, Python, Ruby, PHP, JavaScript, ... tous utilisent un null similaire.

Pourquoi ? Eh bien, quelle est l'alternative?

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

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

Maintenant, est-ce que le C, Java, Python original, Ruby ou PHP supportait l'une ou l'autre de ces fonctionnalités? Non. Les génériques imparfaits de Java sont récents dans l'histoire de la langue et je doute que les autres les mettent en œuvre.

Voilà. null est facile, les types de données algébriques paramétriques sont plus difficiles. Les gens ont opté pour l'alternative la plus simple.

7
Matthieu M.

Null/nil/none lui-même n'est pas mauvais.

Si vous regardez son célèbre discours trompeusement nommé "The Billion dollar Mistake", Tony Hoare explique comment autoriser n'importe quelle variable à pouvoir maintenir null était un énorme erreur. L'alternative - en utilisant Options - ne pas se débarrasse en fait des références nulles. Au lieu de cela, il vous permet de spécifier quelles variables peuvent contenir null et lesquelles ne le sont pas.

En fait, avec les langages modernes qui implémentent une gestion correcte des exceptions, les erreurs de déréférencement nul ne sont pas différentes de toute autre exception - vous la trouvez, vous la corrigez. Certaines alternatives aux références nulles (le modèle Null Object par exemple) masquent les erreurs, provoquant l'échec silencieux des choses jusqu'à bien plus tard. À mon avis, c'est beaucoup mieux pour échouer rapidement .

La question est donc de savoir pourquoi les langues ne parviennent pas à implémenter les options? En fait, le langage sans doute le plus populaire de tous les temps C++ a la capacité de définir des variables d'objet qui ne peuvent pas être attribuées NULL. Il s'agit d'une solution au "problème nul" mentionné par Tony Hoare dans son discours. Pourquoi le prochain langage tapé 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 non.

5
B T

Parce que les langages de programmation sont généralement conçus pour être pratiquement utiles plutôt que techniquement corrects. Le fait est que les états null sont une occurrence courante 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 compliquées que de simplement autoriser des états nuls et de supprimer 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 pseudocode comme:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

et il échouera simplement si joebloggs.txt n'existe pas. Le truc, c'est que pour des scripts simples, ça va probablement et pour de nombreuses situations dans un code plus complexe, je sais qu'il existe et que l'échec ne se produira pas, ce qui m'oblige à vérifier mon temps perdu. Les alternatives plus sûres atteignent leur sécurité en m'obligeant à gérer correctement l'état d'échec potentiel, mais souvent je ne veux pas le faire, je veux juste continuer.

4
Jack Aidley

Il existe des utilisations claires et pratiques de NULL (ou nil, ou Nil, ou null, ou Nothing ou quoi que ce soit d'autre) appelé dans votre langue préférée) pointeur.

Pour les langues qui n'ont pas de système d'exception (par exemple C), un pointeur nul 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 NULL renvoyé par malloc(3) est utilisé comme marqueur d'échec.

Lorsqu'il est utilisé dans les arguments de méthode/fonction, il peut indiquer l'utilisation par défaut de l'argument ou ignorer l'argument de sortie. Exemple ci-dessous.

Même pour les langages avec mécanisme d'exception, un pointeur nul peut être utilisé pour indiquer une erreur logicielle (c'est-à-dire des 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 plantage du programme s'il n'est pas intercepté. Cela élimine les tentatives folles comme Java a et a un meilleur contrôle du flux de programme car les erreurs logicielles ne sont pas interrompues (et les quelques exceptions dures restantes ne sont généralement pas récupérables et ne sont pas détectées))

4
Maxthon Chan

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

  1. null devrait-il exister? Ou devez-vous toujours utiliser Maybe<T> Lorsque null est utile?
  2. Toutes les références doivent-elles être annulables? Sinon, quelle devrait être la valeur par défaut?

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

Je suis au moins d'accord avec vous que toutes les références ne doivent pas être annulées. Mais éviter null n'est pas sans sa complexité:

.NET initialise tous les champs à default<T> Avant qu'ils ne soient accessibles pour la première fois par du code managé. Cela signifie que pour les types de référence, vous avez besoin de null ou quelque chose d'équivalent et que les types de valeur peuvent être initialisés à une sorte de zéro sans exécuter de code. Bien que ces deux éléments présentent de graves inconvénients, la simplicité de l'initialisation de default a peut-être dépassé ces inconvénients.

  • Pour champs d'instance vous pouvez contourner ce problème en exigeant l'initialisation des champs avant d'exposer le pointeur this au code managé. Spec # a emprunté cette voie, en utilisant une syntaxe différente du chaînage du constructeur par rapport à C #.

  • Pour champs statiques, assurez-vous que cela est plus difficile, sauf si vous posez des restrictions strictes sur le type de code qui peut s'exécuter dans un initialiseur de champ, car vous ne pouvez pas simplement masquer le pointeur this.

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

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

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

4
CodesInChaos

La plupart des langages de programmation utiles permettent d'écrire et de lire des éléments de données dans des séquences arbitraires, de sorte qu'il sera souvent impossible de déterminer statiquement l'ordre dans lequel les lectures et les écritures se produiront avant l'exécution d'un programme. Il existe de nombreux cas où le code stockera en fait des données utiles dans chaque emplacement avant de le lire, mais où cela serait difficile à prouver. Ainsi, il sera souvent nécessaire d'exécuter des programmes où il serait au moins théoriquement possible que le code tente 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 aucun moyen général d'empêcher le code de faire la tentative. La seule question est de savoir ce qui devrait arriver quand cela se produit.

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

  • Une approche consisterait à dire que toute tentative de lecture de quelque chose qui n'a pas été écrit entraînera une erreur immédiate.

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

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

  • 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 devrait être la valeur par défaut pour un type de référence. Pour les types de référence immuables, il serait dans de nombreux cas logique de définir une instance par défaut, et de dire que la valeur par défaut pour toute variable de ce type devrait être une référence à cette instance. Pour les types de référence mutables, cependant, cela ne serait pas très utile. Si une tentative est faite pour utiliser un type de référence mutable avant qu'il ne soit écrit, il n'y a généralement pas de ligne de conduite sûre, sauf pour intercepter au point de tentative d'utilisation.

Sémantiquement, si on a un tableau customers de type Customer[20], Et qu'on essaie Customer[4].GiveMoney(23) sans avoir rien stocké dans Customer[4], L'exécution va avoir piéger. On pourrait faire valoir qu'une tentative de lecture de Customer[4] Devrait être immédiatement interrompue, plutôt que d'attendre que le code tente de GiveMoney, mais il y a suffisamment de cas où il est utile de lire un slot, découvrez qu'il ne le fait pas 't tenir une valeur, puis utiliser ces 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'un null doit déclencher une interruption immédiate. C'est une fonctionnalité utile. En général, cependant, tout langage qui permet aux programmeurs de créer des tableaux de références devra soit permettre la possibilité d'éléments de tableau nuls, soit forcer l'initialisation des éléments de tableau à des données qui ne peuvent pas être significatives.

2
supercat