Pourquoi la plupart des langages impératifs / OO «bien connus» permettent un accès non contrôlé à des types qui peuvent représenter une valeur «rien»?


29

J'ai lu sur la (non) commodité d'avoir nullau lieu de (par exemple) Maybe. Après avoir lu cet article , je suis convaincu qu'il serait préférable de l'utiliserMaybe (ou quelque chose de similaire). Cependant, je suis surpris de voir que tous les langages de programmation impératifs ou orientés objet "bien connus" utilisent toujours null(ce qui permet un accès non contrôlé à des types qui peuvent représenter une valeur "rien"), et qui Maybeest principalement utilisé dans les langages de programmation fonctionnels.

Par exemple, regardez le code C # suivant:

void doSomething(string username)
{
    // Check that username is not null
    // Do something
}

Quelque chose sent mauvais ici ... Pourquoi devrions-nous vérifier si l'argument est nul? Ne devrions-nous pas supposer que chaque variable contient une référence à un objet? Comme vous pouvez le voir, le problème est que, par définition, presque toutes les variables peuvent contenir une référence nulle. Et si nous pouvions décider quelles variables sont "nulles" et lesquelles ne le sont pas? Cela nous permettrait d'économiser beaucoup d'efforts lors du débogage et de la recherche d'une "NullReferenceException". Imaginez que, par défaut, aucun type ne puisse contenir une référence nulle . Au lieu de cela, vous déclareriez explicitement qu'une variable peut contenir une référence nulle , uniquement si vous en avez vraiment besoin. Telle est l'idée derrière Peut-être. Si vous avez une fonction qui échoue dans certains cas (par exemple la division par zéro), vous pouvez retourner unMaybe<int>, indiquant explicitement que le résultat peut être un entier, mais aussi rien! C'est une des raisons de préférer Peut-être au lieu de null. Si vous êtes intéressé par plus d'exemples, je vous suggère de lire cet article .

Les faits sont que, malgré les inconvénients de rendre la plupart des types annulables par défaut, la plupart des langages de programmation OO le font. C'est pourquoi je m'interroge sur:

  • Quel genre d'arguments auriez-vous à implémenter nulldans votre langage de programmation à la place Maybe? Y a-t-il des raisons ou s'agit-il simplement de «bagages historiques»?

Veuillez vous assurer de bien comprendre la différence entre null et peut-être avant de répondre à cette question.


3
Je suggère de lire sur Tony Hoare , en particulier son erreur d'un milliard de dollars.
Oded

2
Eh bien, c'était sa raison. Et le résultat malheureux est que c'était une erreur copiée dans la plupart des langues qui ont suivi, jusqu'à aujourd'hui. Il y a des langues où nullou son concept n'existe pas (IIRC Haskell en est un exemple).
Oded

9
Les bagages historiques ne sont pas quelque chose à sous-estimer. Et rappelez-vous que les systèmes d'exploitation sur lesquels ils s'appuient ont été écrits dans des langues qui contiennent des nulls depuis longtemps. Ce n'est pas facile de laisser tomber ça.
Oded

3
Je pense que vous devriez faire la lumière sur le concept de Peut-être au lieu de poster un lien.
JeffO

1
@ GlenH7 Les chaînes en C # sont des valeurs de référence (elles peuvent être nulles). Je sais que int est une valeur primitive, mais il était utile de montrer l'utilisation de peut-être.
aochagavia

Réponses:


15

Je pense que c'est avant tout un bagage historique.

Le langage le plus important et le plus ancien avec nullest le C et le C ++. Mais ici, nullça a du sens. Les pointeurs sont encore un concept assez numérique et de bas niveau. Et comment quelqu'un d'autre a dit, dans l'état d'esprit des programmeurs C et C ++, avoir à dire explicitement que le pointeur peut être nulln'a pas de sens.

Java vient en deuxième position. Considérant que les développeurs Java essayaient de se rapprocher du C ++, afin qu'ils puissent faire une transition du C ++ vers Java plus simple, ils ne voulaient probablement pas jouer avec un tel concept de base du langage. En outre, l'implémentation explicite nullnécessiterait beaucoup plus d'efforts, car vous devez vérifier si la référence non nulle est réellement définie correctement après l'initialisation.

Toutes les autres langues sont identiques à Java. Ils copient généralement la façon dont C ++ ou Java le font, et compte tenu de la façon dont le concept de base nulldes types implicites de référence est, il devient vraiment difficile de concevoir un langage utilisant explicitement null.


"Ils ne voulaient probablement pas jouer avec un tel concept de base du langage". Ils ont déjà complètement supprimé les pointeurs, également supprimer nullne serait pas un gros changement, je pense.
svick

2
@svick Pour Java, les références remplacent les pointeurs. Et dans de nombreux cas, les pointeurs en C ++ ont été utilisés de la même manière que les références Java. Certaines personnes prétendent même que Java a des pointeurs ( programmers.stackexchange.com/questions/207196/… )
Euphoric

J'ai marqué cela comme la bonne réponse. Je voudrais le voter mais je n'ai pas assez de réputation.
aochagavia

1
Se mettre d'accord. Notez qu'en C ++ (contrairement à Java et C), être null-able est l'exception. std::stringne peut pas être null. int&ne peut pas être null. Une int*boîte et C ++ autorisent un accès non contrôlé pour deux raisons: 1. parce que C l'a fait et 2. parce que vous êtes censé comprendre ce que vous faites lorsque vous utilisez des pointeurs bruts en C ++.
MSalters

@MSalters: Si un type n'a pas de valeur par défaut copiable en bits, la création d'un tableau de ce type nécessitera d'appeler un constructeur pour chaque élément de celui-ci avant d'autoriser l'accès au tableau lui-même. Cela peut nécessiter un travail inutile (si certains ou tous les éléments seront écrasés avant d'être lus), peut introduire des complications si le constructeur d'un élément ultérieur échoue après la construction d'un élément antérieur, et peut finir par ne pas vraiment faire grand-chose de toute façon (si une valeur appropriée pour certains éléments du tableau n'a pas pu être déterminée sans en lire d'autres).
supercat

15

En fait, nullc'est une excellente idée. Étant donné un pointeur, nous voulons désigner que ce pointeur ne fait pas référence à une valeur valide. Nous prenons donc un emplacement de mémoire, le déclarons invalide et respectons cette convention (une convention parfois appliquée avec des erreurs de segmentation). Maintenant, chaque fois que j'ai un pointeur, je peux vérifier s'il contient Nothing( ptr == null) ou Some(value)( ptr != null, value = *ptr). Je veux que vous compreniez que cela équivaut à un Maybetype.

Les problèmes avec ceci sont:

  1. Dans de nombreux langages, le système de type n'aide pas ici à garantir une référence non nulle.

    Il s'agit d'un bagage historique, car de nombreux langages impératifs ou POO traditionnels n'ont eu que des avancées incrémentielles dans leurs systèmes de types par rapport aux prédécesseurs. Les petits changements ont l'avantage que les nouvelles langues sont plus faciles à apprendre. C # est un langage courant qui a introduit des outils au niveau du langage pour mieux gérer les valeurs nulles.

  2. Les concepteurs d'API peuvent revenir nullen cas d'échec, mais pas une référence à la réalité même en cas de succès. Souvent, la chose (sans référence) est retournée directement. Cet aplatissement d'un niveau de pointeur rend impossible l'utilisation nullcomme valeur.

    C'est juste de la paresse du côté du concepteur et ne peut pas être aidé sans imposer une imbrication appropriée avec un système de type approprié. Certaines personnes peuvent également essayer de justifier cela par des considérations de performances ou par l'existence de vérifications facultatives (une collection peut renvoyer nullou l'élément lui-même, mais également fournir une containsméthode).

  3. À Haskell, il y a une vue nette sur le Maybetype en tant que monade. Cela facilite la composition des transformations sur la valeur contenue.

    D'un autre côté, les langages de bas niveau comme C traitent à peine les tableaux comme un type distinct, donc je ne sais pas trop à quoi nous nous attendons. Dans les langages POO avec polymorphisme paramétré, un Maybetype vérifié à l'exécution est plutôt trivial à implémenter.


"C # éloigne les références nulles." Quelles sont ces étapes? Je n'ai rien vu de tel dans les changements de langue. Ou voulez-vous simplement dire que les bibliothèques communes utilisent nullmoins qu'elles ne le faisaient dans le passé?
svick

@svick Mauvaise formulation de ma part. « C # est un langage courant qui a introduit des outils au niveau du langage pour mieux gérer les nulls » - je parle des Nullabletypes et de l' ??opérateur par défaut. Il ne résout pas le problème en ce moment en présence de code existant, mais il est un pas vers un avenir meilleur.
amon

Je suis d'accord avec toi. Je voterais pour votre réponse, mais je n'ai pas assez de réputation :( Cependant, Nullable ne fonctionne que pour les types primitifs. Ce n'est donc qu'un petit pas.
aochagavia

1
@svick Nullable n'a rien à voir avec cela. Nous parlons de tous les types de référence autorisant implicitement une valeur nulle, au lieu d'avoir programmeur la définir explicitement. Et Nullable ne peut être utilisé que sur des types de valeur.
Euphoric

@ Euphoric Je pense que votre commentaire était censé être une réponse à amon, je n'ai pas mentionné Nullable.
svick

9

Ma compréhension est que nullc'était une construction nécessaire afin d'abstraire les langages de programmation de l'assemblage. 1 Les programmeurs devaient avoir la capacité d'indiquer qu'un pointeur ou une valeur de registre était not a valid valueet est nulldevenu le terme courant pour cette signification.

En renforçant le point qui nulln'est qu'une convention pour représenter un concept, la valeur réelle pour nullutilisé peut / peut varier en fonction du langage de programmation et de la plate-forme.

Si vous conceviez un nouveau langage et que vous vouliez éviter nullmais utiliser à la maybeplace, j'encouragerais un terme plus descriptif tel que not a valid valueou navvpour indiquer l'intention. Mais le nom de cette non-valeur est un concept distinct de si vous devez autoriser les non-valeurs à exister même dans votre langue.

Avant de pouvoir décider de l'un de ces deux points, vous devez définir ce que la signification de maybesignifierait pour votre système. Vous pouvez trouver que c'est juste un renommage de nullla signification de not a valid valueou vous pouvez trouver qu'il a une sémantique différente pour votre langue.

De même, la décision de vérifier l'accès par rapport à l' nullaccès ou à la référence est une autre décision de conception de votre langue.

Pour fournir un peu d'histoire, Csupposait implicitement que les programmeurs comprenaient ce qu'ils tentaient de faire lorsqu'ils manipulaient la mémoire. Comme il s'agissait d'une abstraction supérieure à l'assemblage et aux langages impératifs qui l'ont précédé, je me risquerais à penser que l'idée de protéger le programmeur d'une référence incorrecte ne lui était pas venue à l'esprit.

Je crois que certains compilateurs ou leurs outils supplémentaires peuvent fournir une mesure de vérification contre l'accès au pointeur non valide. D'autres ont donc noté ce problème potentiel et pris des mesures pour s'en protéger.

Que vous puissiez ou non l'autoriser dépend de ce que vous voulez que votre langue accomplisse et du degré de responsabilité que vous souhaitez imposer aux utilisateurs de votre langue. Cela dépend également de votre capacité à créer un compilateur pour restreindre ce type de comportement.

Alors pour répondre à vos questions:

  1. "Quel genre d'arguments ..." - Eh bien, cela dépend de ce que vous voulez que le langage fasse. Si vous souhaitez simuler l'accès au bare-metal, vous pouvez l'autoriser.

  2. "Est-ce juste un bagage historique?" Peut-être, peut-être pas. nullavait certainement / a un sens pour un certain nombre de langues et contribue à l'expression de ces langues. Un précédent historique peut avoir affecté des langues plus récentes et leur acceptation, nullmais c'est un peu beaucoup de faire signe de la main et de déclarer le concept de bagage historique inutile.


1 Voir cet article Wikipédia bien que Hoare soit reconnu pour les valeurs nulles et les langages orientés objet. Je crois que les langues impératives ont progressé le long d'un arbre généalogique différent de celui d'Algol.


Le fait est que la plupart des variables en C # ou Java, par exemple, peuvent recevoir une référence nulle. Il semble qu'il serait beaucoup mieux d'attribuer des références nulles uniquement aux objets qui indiquent explicitement que "Peut-être" il n'y a pas de référence. Donc, ma question concerne le «concept» nul, et non le mot.
aochagavia

2
«Les références de pointeur nul peuvent apparaître comme des erreurs lors de la compilation» Quels compilateurs peuvent faire cela?
svick

Pour être tout à fait juste, vous n'affectez jamais une référence nulle à un objet ... la référence à l'objet n'existe tout simplement pas (la référence ne pointe vers rien (0x000000000), ce qui est par définition null).
mgw854

La citation de la spécification C99 parle du caractère nul , pas du pointeur nul , ce sont deux concepts très différents.
svick

2
@ GlenH7 Cela ne fait pas ça pour moi. Le code se object o = null; o.ToString();compile très bien pour moi, sans erreurs ni avertissements dans VS2012. ReSharper se plaint de cela, mais ce n'est pas le compilateur.
svick

7

Si vous regardez les exemples de l'article que vous avez cité, la plupart du temps, l'utilisation Maybene raccourcit pas le code. Cela n'empêche pas la nécessité de vérifier Nothing. La seule différence est qu'il vous rappelle de le faire via le système de type.

Remarque, je dis "rappelez-vous", pas forcer. Les programmeurs sont paresseux. Si un programmeur est convaincu qu'une valeur ne peut pas l'être Nothing, il va déréférencer le Maybesans le vérifier, tout comme il déréférence un pointeur nul maintenant. Le résultat final est que vous convertissez une exception de pointeur nul en une exception "peut-être vide déréférencé".

Le même principe de la nature humaine s'applique dans d'autres domaines où les langages de programmation tentent de forcer les programmeurs à faire quelque chose. Par exemple, les concepteurs Java ont essayé de forcer les gens à gérer la plupart des exceptions, ce qui a donné lieu à un grand nombre de références qui ignorent silencieusement ou propagent aveuglément les exceptions.

Ce qui Maybeest bien, c'est quand beaucoup de décisions sont prises via la correspondance de motifs et le polymorphisme au lieu de vérifications explicites. Par exemple, vous pouvez créer des fonctions distinctes processData(Some<T>)et processData(Nothing<T>), ce que vous ne pouvez pas faire null. Vous déplacez automatiquement votre gestion des erreurs vers une fonction distincte, ce qui est très souhaitable dans la programmation fonctionnelle où les fonctions sont transmises et évaluées paresseusement plutôt que d'être toujours appelées de manière descendante. Dans la POO, la façon préférée de découpler votre code de gestion des erreurs est avec des exceptions.


Pensez-vous que ce serait une fonctionnalité souhaitable dans un nouveau langage OO?
aochagavia

Vous pouvez l'implémenter vous-même si vous souhaitez obtenir les avantages du polymorphisme. La seule chose pour laquelle vous avez besoin d'une prise en charge linguistique est la non-nullité. Je serais ravi de voir un moyen de spécifier cela pour vous-même, similaire const, mais de le rendre facultatif. Certains types de code de niveau inférieur, comme les listes liées par exemple, seraient très ennuyeux à implémenter avec des objets non nullables.
Karl Bielefeldt

2
Vous n'avez cependant pas à vérifier avec le type Maybe. Le type Maybe est une monade, il doit donc avoir les fonctions map :: Maybe a -> (a -> b) et être bind :: Maybe a -> (a -> Maybe b)défini dessus, vous pouvez donc continuer et effectuer d'autres calculs pour la plupart avec une refonte à l'aide d'une instruction if else. Et getValueOrDefault :: Maybe a -> (() -> a) -> avous permet de gérer le cas nullable. C'est beaucoup plus élégant que la correspondance de motifs sur le Maybe aexplicitement.
DetriusXii

1

Maybeest une façon très fonctionnelle de penser un problème - il y a une chose, et elle peut ou non avoir une valeur définie. Dans un sens orienté objet, cependant, nous remplaçons cette idée d'une chose (qu'elle ait ou non une valeur) par un objet. De toute évidence, un objet a une valeur. Si ce n'est pas le cas, nous disons que l'objet l'est null, mais ce que nous voulons vraiment dire, c'est qu'il n'y a aucun objet du tout. La référence que nous avons à l'objet ne signifie rien. La traduction Maybeen un concept OO n'a rien de nouveau - en fait, cela crée simplement un code plus encombré. Vous devez toujours avoir une référence nulle pour la valeur duMaybe<T>. Vous devez toujours faire des vérifications nulles (en fait, vous devez faire beaucoup plus de vérifications nulles, encombrant votre code), même si elles sont maintenant appelées "peut-être des vérifications". Bien sûr, vous écrirez du code plus robuste comme le prétend l'auteur, mais je dirais que ce n'est que le cas parce que vous avez rendu le langage beaucoup plus abstrait et obtus, nécessitant un niveau de travail inutile dans la plupart des cas. Je prendrais volontiers une NullReferenceException de temps en temps plutôt que de traiter du code spaghetti en faisant une vérification Peut-être à chaque fois que j'accède à une nouvelle variable.


2
Je pense que cela conduirait à moins de contrôles nuls, car il suffit de vérifier si vous voyez un peut-être <T> et de ne pas vous soucier du reste des types.
aochagavia

1
@svick Maybe<T>doit autoriser nullcomme valeur, car la valeur peut ne pas exister. Si je l'ai Maybe<MyClass>, et qu'il n'a pas de valeur, le champ de valeur doit contenir une référence nulle. Il n'y a rien d'autre pour que ce soit vraiment sûr.
mgw854

1
@ mgw854 Bien sûr qu'il y en a. En langage OO, Maybepourrait être une classe abstraite, avec deux classes qui en héritent: Some(qui a un champ pour la valeur) et None(qui n'a pas ce champ). De cette façon, la valeur n'est jamais null.
svick

6
Avec "non-nullability" par défaut, et peut-être <T>, vous pouvez être certain que certaines variables contiennent toujours des objets. À savoir, toutes les variables qui ne sont peut-être pas <T>
aochagavia

3
@ mgw854 Le point de ce changement serait une puissance expressive accrue pour le développeur. À l'heure actuelle, le développeur doit toujours supposer que la référence ou le pointeur peut être nul et doit vérifier qu'il existe une valeur utilisable. En implémentant cette modification, vous donnez au développeur le pouvoir de dire qu'il a vraiment besoin d'une valeur valide et que le compilateur vérifie que la valeur valide a été transmise. Mais en donnant toujours au développeur la possibilité de s'inscrire et de ne pas laisser passer de valeur.
Euphoric

1

Le concept de nullremonte facilement à C, mais ce n'est pas là que réside le problème.

Mon langage de tous les jours est C # et je garderais nullune différence. C # a deux types de types, valeurs et références . Les valeurs ne peuvent jamais exister null, mais il y a des moments où j'aimerais pouvoir exprimer qu'aucune valeur ne convient parfaitement. Pour ce faire, C # utilise des Nullabletypes, ce intserait donc la valeur et int?la valeur nullable. C'est ainsi que je pense que les types de référence devraient également fonctionner.

Voir aussi: La référence nulle n'est peut-être pas une erreur :

Les références nulles sont utiles et parfois indispensables (considérez les problèmes si vous pouvez ou non renvoyer une chaîne en C ++). L'erreur n'est vraiment pas dans l'existence des pointeurs nuls, mais dans la façon dont le système de types les gère. Malheureusement, la plupart des langages (C ++, Java, C #) ne les gèrent pas correctement.


0

Je pense que c'est parce que la programmation fonctionnelle se préoccupe beaucoup des types , en particulier des types qui combinent d'autres types (tuples, fonctions comme types de première classe, monades, etc.) que la programmation orientée objet (ou du moins initialement).

Je pense que les versions modernes des langages de programmation dont vous parlez (C ++, C #, Java) sont toutes basées sur des langages qui n'avaient aucune forme de programmation générique (C, C # 1.0, Java 1). Sans cela, vous pouvez toujours faire une sorte de différence entre les objets nullable et non nullable dans le langage (comme les références C ++, qui ne peuvent pas être null, mais sont également limitées), mais c'est beaucoup moins naturel.


Je pense qu'en cas de programmation fonctionnelle, c'est le cas de FP n'ayant pas de type référence ou pointeur. Dans FP, tout est une valeur. Et si vous avez un type de pointeur, il devient facile de dire "pointeur vers rien".
Euphoric

0

Je pense que la raison fondamentale est que relativement peu de contrôles nuls sont nécessaires pour rendre un programme "sûr" contre la corruption de données. Si un programme essaie d'utiliser le contenu d'un élément de tableau ou d'un autre emplacement de stockage qui est censé avoir été écrit avec une référence valide mais ne l'a pas été, le meilleur cas est qu'une exception soit levée. Idéalement, l'exception indiquera exactement où le problème s'est produit, mais ce qui importe, c'est qu'une sorte d'exception soit levée avant que la référence nulle ne soit stockée quelque part, ce qui pourrait endommager les données. À moins qu'une méthode ne stocke un objet sans essayer de l'utiliser d'une manière ou d'une autre, une tentative d'utilisation d'un objet constituera - en soi - une sorte de «contrôle nul».

Si l'on veut s'assurer qu'une référence nulle qui apparaît là où elle ne devrait pas provoquer une exception particulière autre que NullReferenceException, il sera souvent nécessaire d'inclure des vérifications nulles partout. D'un autre côté, le simple fait de garantir qu'une exception se produira avant qu'une référence nulle puisse causer des "dommages" au-delà de tout ce qui a déjà été fait nécessitera souvent relativement peu de tests - les tests ne seront généralement requis que dans les cas où un objet stockerait une référence sans essayer de l'utiliser, et soit la référence nulle écraserait une référence valide, soit elle ferait en sorte qu'un autre code interpréterait mal d'autres aspects de l'état du programme. De telles situations existent, mais ne sont pas si courantes; la plupart des références nulles accidentelles seront capturées très rapidementque l'on vérifie ou non .


ce post est assez difficile à lire (mur de texte). Pourriez-vous le modifier sous une meilleure forme?
moucher

1
@gnat: Est-ce mieux?
supercat

0

" Maybe," comme écrit, est une construction de niveau supérieur à null. En utilisant plus de mots pour le définir, Peut-être est "un pointeur vers une chose ou un pointeur vers rien, mais le compilateur n'a pas encore reçu suffisamment d'informations pour déterminer laquelle." Cela vous oblige à vérifier explicitement chaque valeur en permanence, sauf si vous créez une spécification de compilateur suffisamment intelligente pour suivre le code que vous écrivez.

Vous pouvez facilement réaliser une implémentation de Maybe avec un langage qui a des valeurs nulles. C ++ en a un sous la forme de boost::optional<T>. Faire l'équivalent de null avec Maybe est très difficile. En particulier, si j'ai unMaybe<Just<T>> , je ne peux pas l'affecter à null (car un tel concept n'existe pas), tandis qu'un T**dans une langue avec null est très facile à affecter à null. Cela oblige à utiliser Maybe<Maybe<T>>, ce qui est totalement valide, mais vous obligera à faire beaucoup plus de vérifications pour utiliser cet objet.

Certains langages fonctionnels utilisent Peut-être parce que null nécessite un comportement non défini ou une gestion des exceptions, aucun des deux n'est un concept facile à mapper dans les syntaxes du langage fonctionnel. Peut-être remplit-il mieux le rôle dans de telles situations fonctionnelles, mais dans les langages procéduraux, null est roi. Ce n'est pas une question de bien ou de mal, mais simplement de savoir ce qui permet de dire plus facilement à l'ordinateur ce que vous voulez qu'il fasse.

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.