C'est l'une des règles qui ne cessent de se répéter et qui me rendent perplexe.
Les nuls sont mauvais et doivent être évités autant que possible .
Mais, mais - de ma naïveté, permettez-moi de crier - parfois une valeur PEUT être significativement absente!
S'il vous plaît laissez-moi vous poser cette question sur un exemple qui provient de ce code horrible anti-modèle monté sur lequel je travaille en ce moment . Il s'agit, à la base, d'un jeu multijoueur au tour par tour sur le Web, où les tours des deux joueurs sont exécutés simultanément (comme dans Pokemon, et contrairement aux échecs).
Après chaque tour, le serveur diffuse une liste de mises à jour du code JS côté client. La classe Update:
public class GameUpdate
{
public Player player;
// other stuff
}
Cette classe est sérialisée en JSON et envoyée aux joueurs connectés.
La plupart des mises à jour ont naturellement un joueur associé - après tout, il est nécessaire de savoir quel joueur a fait quel mouvement ce tour-ci. Cependant, certaines mises à jour ne peuvent pas être associées de manière significative à un lecteur. Exemple: le jeu a été lié de force car la limite de tour sans action a été dépassée. Pour de telles mises à jour, je pense qu'il est significatif que le lecteur soit annulé.
Bien sûr, je pourrais "corriger" ce code en utilisant l'héritage:
public class GameUpdate
{
// stuff
}
public class GamePlayerUpdate : GameUpdate
{
public Player player;
// other stuff
}
Cependant, je ne vois pas en quoi cela peut être amélioré, pour deux raisons:
- Le code JS recevra désormais simplement un objet sans Player en tant que propriété définie, ce qui est le même que s'il était nul puisque les deux cas nécessitent de vérifier si la valeur est présente ou absente;
- Si un autre champ nullable était ajouté à la classe GameUpdate, je devrais être en mesure d'utiliser l'héritage multiple pour continuer avec cette conception - mais MI est mauvais en soi (selon des programmeurs plus expérimentés) et plus important encore, C # ne le fait pas l'avoir donc je ne peux pas l'utiliser.
J'ai une idée que ce morceau de ce code est l'un des très nombreux où un bon programmeur expérimenté hurlerait d'horreur. En même temps, je ne vois pas comment, dans cet endroit, est-ce que cela peut nuire à quoi que ce soit et ce qui devrait être fait à la place.
Pourriez-vous m'expliquer ce problème?
la source
Réponses:
Beaucoup de choses valent mieux que null.
(qui est ce qui vous a fait considérer comme nul en premier lieu)
L'astuce consiste à savoir quand le faire. Null est vraiment bon pour exploser dans votre visage, mais seulement lorsque vous le détachez sans le vérifier. Ce n'est pas bon pour expliquer pourquoi.
Lorsque vous n'avez pas besoin d'exploser et que vous ne voulez pas vérifier la valeur null, utilisez l'une de ces alternatives. Cela peut sembler bizarre de renvoyer une collection si elle ne contient que 0 ou 1 élément, mais elle est vraiment bonne pour vous permettre de traiter silencieusement les valeurs manquantes.
Les monades ont l'air fantaisistes, mais ici, tout ce qu'elles font, c'est de vous faire comprendre que les tailles valides pour la collection sont 0 et 1. Si vous les avez, considérez-les.
N'importe lequel de ceux-ci fait un meilleur travail pour clarifier votre intention que null. C'est la différence la plus importante.
Il peut être utile de comprendre pourquoi vous revenez du tout plutôt que de simplement lever une exception. Les exceptions sont essentiellement un moyen de rejeter une hypothèse (ce n'est pas le seul moyen). Lorsque vous demandez le point où deux lignes se croisent, vous supposez qu'elles se croisent. Ils pourraient être parallèles. Devriez-vous lever une exception "lignes parallèles"? Eh bien, vous pourriez mais maintenant vous devez gérer cette exception ailleurs ou la laisser arrêter le système.
Si vous préférez rester où vous êtes, vous pouvez transformer le rejet de cette hypothèse en une sorte de valeur qui peut exprimer des résultats ou des rejets. Ce n'est pas si étrange. Nous le faisons tout le temps en algèbre . L'astuce consiste à rendre cette valeur significative afin que nous puissions comprendre ce qui s'est passé.
Si la valeur retournée peut exprimer des résultats et des rejets, elle doit être envoyée à un code qui peut gérer les deux. Le polymorphisme est vraiment puissant à utiliser ici. Plutôt que de simplement échanger des chèques nuls contre des
isPresent()
chèques, vous pouvez écrire du code qui se comporte bien dans les deux cas. Soit en remplaçant les valeurs vides par des valeurs par défaut, soit en ne faisant rien.Le problème avec null est qu'il peut signifier beaucoup trop de choses. Cela finit donc par détruire des informations.
Dans mon expérience de pensée nulle préférée, je vous demande d'imaginer une machine Rube Goldbergienne complexe qui signale les messages codés en utilisant la lumière colorée en ramassant les ampoules colorées d'un tapis roulant, en les vissant dans une prise et en les alimentant. Le signal est contrôlé par les différentes couleurs des ampoules placées sur la bande transporteuse.
On vous a demandé de rendre cette chose horriblement chère conforme à la RFC3.14159 qui stipule qu'il devrait y avoir un marqueur entre les messages. Le marqueur ne doit pas être du tout lumineux. Cela devrait durer exactement une impulsion. La même quantité de temps qu'une lumière colorée unique brille normalement.
Vous ne pouvez pas simplement laisser des espaces entre les messages car l'engin déclenche des alarmes et s'arrête s'il ne trouve pas une ampoule à mettre dans la douille.
Tous ceux qui connaissent ce projet frissonnent à l'idée de toucher la machine ou son code de contrôle. Ce n'est pas facile de changer. Que faire? Eh bien, vous pouvez plonger et commencer à casser des choses. Peut-être mettre à jour ce design hideux. Ouais tu pourrais faire tellement mieux. Ou vous pouvez parler au concierge et commencer à ramasser les ampoules grillées.
Voilà la solution polymorphe. Le système n'a même pas besoin de savoir que quelque chose a changé.
C'est vraiment bien si vous pouvez retirer cela. En encodant un rejet significatif d'une hypothèse en une valeur, vous n'avez pas à forcer la question à changer.
Combien de pommes me devrez-vous si je vous donne 1 pomme? -1. Parce que je te devais 2 pommes d'hier. Ici, l'hypothèse de la question "vous me devez" est rejetée avec un nombre négatif. Même si la question était erronée, des informations significatives sont encodées dans cette réponse. En la rejetant de manière significative, la question n'est pas obligée de changer.
Cependant, changer la question est parfois la voie à suivre. Si l'hypothèse peut être rendue plus fiable, tout en restant utile, envisagez de le faire.
Votre problème est que, normalement, les mises à jour proviennent des joueurs. Mais vous avez découvert le besoin d'une mise à jour qui ne vient pas du joueur 1 ou du joueur 2. Vous avez besoin d'une mise à jour indiquant que le temps est écoulé. Aucun joueur ne dit cela, alors que devez-vous faire? Laisser le joueur nul semble si tentant. Mais cela détruit l'information. Vous essayez de coder la connaissance dans un trou noir.
Le champ du joueur ne vous dit pas qui vient de bouger. Vous pensez que oui, mais ce problème prouve que c'est un mensonge. Le champ du joueur vous indique d'où vient la mise à jour. Votre mise à jour expirée provient de l'horloge du jeu. Ma recommandation est de donner à l'horloge le crédit qu'elle mérite et de changer le nom du
player
champ en quelque chose commesource
.De cette façon, si le serveur s'arrête et doit suspendre le jeu, il peut envoyer une mise à jour qui signale ce qui se passe et se répertorie comme source de la mise à jour.
Est-ce à dire que vous ne devez jamais laisser de côté quelque chose? Non. Mais vous devez considérer dans quelle mesure rien ne peut vraiment être considéré comme signifiant une seule bonne valeur par défaut connue. Rien n'est vraiment facile à utiliser. Soyez donc prudent lorsque vous l'utilisez. Il y a une école de pensée qui embrasse en ne favorisant rien . C'est ce qu'on appelle la convention sur la configuration .
J'aime ça moi-même, mais seulement lorsque la convention est clairement communiquée d'une manière ou d'une autre. Ce ne devrait pas être un moyen de brouiller les débutants. Mais même si vous l'utilisez, null n'est toujours pas la meilleure façon de le faire. Null est un rien dangereux . Null prétend être quelque chose que vous pouvez utiliser jusqu'à ce que vous l'utilisiez, puis il fait sauter un trou dans l'univers (brise votre modèle sémantique). À moins de faire un trou dans l'univers, c'est le comportement dont vous avez besoin, pourquoi jouez-vous avec ça? Utilisez autre chose.
la source
null
ce n'est pas vraiment le problème ici. Le problème est de savoir comment la plupart des langages de programmation actuels gèrent les informations manquantes; ils ne prennent pas ce problème au sérieux (ou du moins ne fournissent pas le support et la sécurité dont la plupart des terriens ont besoin pour éviter les pièges). Cela conduit à tous les problèmes que nous avons appris à craindre (Javas NullPointerException, Segfaults, ...).Voyons comment mieux gérer ce problème.
D'autres ont suggéré le modèle Null-Object . Bien que je pense que cela s'applique parfois, il existe de nombreux scénarios où il n'y a tout simplement pas de valeur par défaut unique. Dans ces cas, les consommateurs des informations éventuellement absentes doivent pouvoir décider eux-mêmes quoi faire si ces informations sont manquantes.
Il existe des langages de programmation qui offrent une sécurité contre les pointeurs nuls (ce qu'on appelle la sécurité nulle) au moment de la compilation. Pour donner quelques exemples modernes: Kotlin, Rust, Swift. Dans ces langues, le problème des informations manquantes a été pris au sérieux et l'utilisation de null est totalement indolore - le compilateur vous empêchera de déréférencer les nullpointers.
En Java, des efforts ont été faits pour établir les annotations @ Nullable / @ NotNull avec les plugins compilateur + IDE pour atteindre le même effet. Ce n'était pas vraiment réussi mais c'est hors de portée pour cette réponse.
Un type générique explicite qui dénote une absence possible est une autre façon de le gérer. Par exemple, en Java, il y en a
java.util.Optional
. Cela peut fonctionner: au niveau d'un projet ou d'une entreprise, vous pouvez établir des règles / directives
java.util.Optional
au lieu denull
Votre communauté / écosystème de langues a peut-être trouvé d'autres moyens de résoudre ce problème mieux que le compilateur / interprète.
la source
Optional.isPresent
isPresent()
n'est pas l'utilisation recommandée de FacultatifJe ne vois aucun mérite dans l'affirmation selon laquelle null est mauvais. J'ai vérifié les discussions à ce sujet et elles ne font que m'impatienter et m'énerver.
Null peut être mal utilisé, en tant que «valeur magique», alors qu'il signifie réellement quelque chose. Mais il faudrait faire une programmation assez tordue pour y arriver.
Null devrait signifier «pas là», qui n'est pas créé (encore) ou (déjà) disparu ou non fourni s'il s'agit d'un argument. Ce n'est pas un état, le tout manque. Dans un environnement OO où les choses sont créées et détruites tout le temps, c'est une condition valable et significative différente de tout état.
Pas entièrement vrai, je peux obtenir un avertissement pour ne pas vérifier la valeur null avant d'utiliser la variable. Et ce n'est pas pareil. Je ne veux peut-être pas épargner les ressources d'un objet qui n'a aucun but. Et plus important encore, je devrai vérifier l'alternative tout de même afin d'obtenir le comportement souhaité. Si j'oublie de le faire, je préfère obtenir une exception NullReferenceException au moment de l'exécution plutôt qu'un comportement non défini / involontaire.
Il y a un problème à prendre en compte. Dans un contexte multithread, vous devez vous protéger contre les conditions de concurrence en attribuant à une variable locale avant d'effectuer la vérification de null.
Non, cela ne signifie / ne devrait rien signifier donc il n'y a rien à détruire. Le nom et le type de la variable / argument diront toujours ce qui manque, c'est tout ce qu'il faut savoir.
Modifier:
Je suis à mi-chemin de la vidéo candied_orange liée à l'une de ses autres réponses sur la programmation fonctionnelle et c'est très intéressant. Je peux en voir l'utilité dans certains scénarios. L'approche fonctionnelle que j'ai vue jusqu'à présent, avec des morceaux de code volant autour, me fait généralement grincer des dents lorsqu'il est utilisé dans un environnement OO. Mais je suppose qu'il a sa place. Quelque part. Et je suppose qu'il y aura des langages qui feront du bon travail en cachant ce que je perçois comme un désordre déroutant chaque fois que je le rencontre en C #, des langages qui fournissent à la place un modèle propre et réalisable.
Si ce modèle fonctionnel est votre état d'esprit / cadre, il n'y a vraiment pas d'autre alternative que de pousser les incidents en tant que descriptions d'erreurs documentées et finalement de laisser l'appelant gérer les ordures accumulées qui ont été traînées tout le long du chemin jusqu'au point d'initiation. Apparemment, c'est exactement comme cela que fonctionne la programmation fonctionnelle. Appelez cela une limitation, appelez-le un nouveau paradigme. Vous pouvez considérer que c'est un hack pour les disciples fonctionnels qui leur permet de continuer un peu plus loin sur le chemin profane, vous pouvez être ravi de cette nouvelle façon de programmer qui ouvre de nouvelles possibilités passionnantes. Quoi qu'il en soit, il me semble inutile d'entrer dans ce monde, de revenir à la façon traditionnelle de faire les choses en OO (oui, nous pouvons lever des exceptions, désolé), de choisir un concept à partir de cela,
Tout concept peut être utilisé dans le mauvais sens. D'où je me tiens, les "solutions" présentées ne sont pas des alternatives pour une utilisation appropriée de null. Ce sont des concepts d'un autre monde.
la source
None
représente…Ta question.
Entièrement à bord avec vous là-bas. Cependant, il y a quelques mises en garde que j'entrerai dans une seconde. Mais d'abord:
Je pense que c'est une déclaration bien intentionnée mais trop généralisée.
Les nuls ne sont pas mauvais. Ils ne sont pas un contre-modèle. Ils ne doivent pas être évités à tout prix. Cependant, les valeurs nulles sont sujettes à erreur (faute de vérifier la valeur nulle).
Mais je ne comprends pas comment le fait de ne pas vérifier null est en quelque sorte la preuve que null est intrinsèquement mauvais à utiliser.
Si null est mauvais, les index de tableau et les pointeurs C ++ le sont aussi. Ils ne sont pas. Ils sont faciles à tirer avec le pied, mais ils ne sont pas censés être évités à tout prix.
Pour le reste de cette réponse, je vais adapter l'intention de "les nuls sont mauvais" à un plus nuancé "les nuls devraient être traités de manière responsable ".
Votre solution.
Bien que je partage votre opinion sur l'utilisation de null comme règle globale; Je ne suis pas d'accord avec votre utilisation de null dans ce cas particulier.
Pour résumer les commentaires ci-dessous: vous comprenez mal le but de l'héritage et du polymorphisme. Bien que votre argument pour utiliser null soit valide, votre "preuve" supplémentaire de la raison pour laquelle l'autre manière est mauvaise est basée sur une mauvaise utilisation de l'héritage, rendant vos points quelque peu hors de propos pour le problème nul.
Si ces mises à jour sont significativement différentes (ce qu'elles sont), vous avez besoin de deux types de mise à jour distincts.
Prenons l'exemple des messages. Vous avez des messages d'erreur et des messages de chat IM. Mais bien qu'ils puissent contenir des données très similaires (un message de chaîne et un expéditeur), ils ne se comportent pas de la même manière. Ils doivent être utilisés séparément, en tant que classes différentes.
Ce que vous faites maintenant, c'est différencier efficacement deux types de mise à jour en fonction de la nullité de la propriété Player. Cela revient à décider entre un message instantané et un message d'erreur basé sur l'existence d'un code d'erreur. Cela fonctionne, mais ce n'est pas la meilleure approche.
Votre argument repose sur des erreurs de polymorphisme. Si vous avez une méthode qui renvoie un
GameUpdate
objet, elle ne devrait généralement pas se soucier de faire la différence entreGameUpdate
,PlayerGameUpdate
ouNewlyDevelopedGameUpdate
.Une classe de base doit avoir un contrat pleinement fonctionnel, c'est-à-dire que ses propriétés sont définies sur la classe de base et non sur la classe dérivée (cela étant dit, la valeur de ces propriétés de base peut bien sûr être remplacée dans les classes dérivées).
Cela rend votre argument théorique. Dans la bonne pratique, vous ne devriez jamais vous soucier que votre
GameUpdate
objet ait ou non un joueur. Si vous essayez d'accéder à laPlayer
propriété d'unGameUpdate
, vous faites quelque chose d'illogique.Les langages typés tels que C # ne vous permettraient même pas d'essayer d'accéder à la
Player
propriété d'unGameUpdate
carGameUpdate
il n'a tout simplement pas la propriété. Javascript est considérablement plus laxiste dans son approche (et il ne nécessite pas de précompilation), il ne prend donc pas la peine de vous corriger et le fait exploser au moment de l'exécution.Vous ne devriez pas créer de classes basées sur l'ajout de propriétés nullables. Vous devez créer des classes basées sur des types de mises à jour de jeu fonctionnellement uniques .
Comment éviter les problèmes avec null en général?
Je pense qu'il est pertinent de préciser comment vous pouvez exprimer de manière significative l'absence de valeur. Il s'agit d'un ensemble de solutions (ou de mauvaises approches) sans ordre particulier.
1. Utilisez null de toute façon.
C'est l'approche la plus simple. Cependant, vous vous inscrivez pour une tonne de vérifications nulles et (en cas d'échec) de dépannage des exceptions de référence null.
2. Utilisez une valeur vide de sens non nulle
Voici un exemple simple
indexOf()
:Au lieu de
null
, vous obtenez-1
. Sur le plan technique, il s'agit d'une valeur entière valide. Cependant, une valeur négative est une réponse absurde car les indices devraient être des entiers positifs.Logiquement, cela ne peut être utilisé que dans les cas où le type de retour autorise plus de valeurs possibles que celles qui peuvent être renvoyées de manière significative (indexOf = entiers positifs; int = nombres négatifs et positifs)
Pour les types de référence, vous pouvez toujours appliquer ce principe. Par exemple, si vous avez une entité avec un ID entier, vous pouvez renvoyer un objet factice avec son ID défini sur -1, par opposition à null.
Vous devez simplement définir un "état factice" par lequel vous pouvez mesurer si un objet est réellement utilisable ou non.
Notez que vous ne devez pas vous fier à des valeurs de chaîne prédéterminées si vous ne contrôlez pas la valeur de chaîne. Vous pouvez envisager de définir le nom d'un utilisateur sur "null" lorsque vous souhaitez renvoyer un objet vide, mais vous rencontrez des problèmes lorsque Christopher Null s'inscrit à votre service .
3. Lancez une exception à la place.
Non, juste non. Les exceptions ne doivent pas être gérées pour le flux logique.
Cela étant dit, dans certains cas, vous ne souhaitez pas masquer l'exception (nullreference), mais plutôt afficher une exception appropriée. Par exemple:
Cela peut lancer un
PersonNotFoundException
(ou similaire) quand il n'y a personne avec ID 123.De manière assez évidente, cela n'est acceptable que dans les cas où le fait de ne pas trouver un article rend impossible tout autre traitement. S'il est impossible de trouver un élément et que l'application peut continuer, vous ne devez JAMAIS lever une exception . Je ne peux insister assez sur ce point.
la source
La raison pour laquelle les valeurs NULL sont mauvaises n'est pas que la valeur manquante est codée comme nulle, mais que toute valeur de référence peut être nulle. Si vous avez C #, vous êtes probablement perdu de toute façon. Je ne vois pas un point ro utiliser certains facultatifs, car un type de référence est déjà facultatif. Mais pour Java, il y a des annotations @Notnull, que vous semblez pouvoir configurer au niveau du package , puis pour les valeurs qui peuvent être manquées, vous pouvez utiliser l'annotation @Nullable.
la source
Les nuls ne sont pas mauvais, il n'y a en réalité aucun moyen de mettre en œuvre l'une des alternatives sans à un moment donné traiter quelque chose qui ressemble à un nul.
Les nuls sont cependant dangereux . Comme toute autre construction de bas niveau, si vous vous trompez, il n'y a rien en dessous pour vous attraper.
Je pense qu'il y a beaucoup d'arguments valides contre null:
Que si vous êtes déjà dans un domaine de haut niveau, il existe des modèles plus sûrs que l'utilisation du modèle de bas niveau.
À moins que vous n'ayez besoin des avantages d'un système de bas niveau et que vous soyez prêt à être prudent, vous ne devriez peut-être pas utiliser un langage de bas niveau.
etc ...
Ceux-ci sont tous valides et ne pas avoir à faire l'effort d'écrire des getters et setters, etc. est considéré comme une raison courante de ne pas avoir suivi ces conseils. Il n'est pas vraiment surprenant que beaucoup de programmeurs soient parvenus à la conclusion que les valeurs nulles sont dangereuses et paresseuses (une mauvaise combinaison!), Mais comme tant d'autres bits de conseils "universels", elles ne sont pas aussi universelles qu'elles sont formulées.
Les bonnes personnes qui ont écrit la langue ont pensé qu'elles seraient utiles à quelqu'un. Si vous comprenez les objections et pensez toujours qu'elles vous conviennent, personne d'autre ne saura mieux.
la source
Java a une
Optional
classe avec unifPresent
+ lambda explicite pour accéder à n'importe quelle valeur en toute sécurité. Cela peut aussi être fait dans JS:Voir cette question StackOverflow .
Soit dit en passant: beaucoup de puristes trouveront cet usage douteux, mais je ne le pense pas. Des fonctionnalités optionnelles existent.
la source
java.lang.Optional
être ne peut-elle pas êtrenull
aussi?Optional.of(null)
lancerait une exception,Optional.ofNullable(null)
donneraitOptional.empty()
- la valeur pas-là.Optional<Type> a = null
?Optional<Optional<Type>> a = Optional.empty();
;) - En d'autres termes, dans JS (et dans d'autres langues), de telles choses ne seront pas réalisables. Scala avec des valeurs par défaut ferait l'affaire. Java peut-être avec une énumération. JS Je doute:Optional<Type> a = Optional.empty();
.null
ou vide, à deux plus quelques méthodes supplémentaires sur l'objet interposé. C'est tout un exploit.La RCA Hoare a déclaré nulle son "erreur d'un milliard de dollars". Cependant, il est important de noter qu'il pensait que la solution n'était pas "pas de null nulle part" mais plutôt "des pointeurs non nullables comme valeur par défaut et des null uniquement là où ils sont sémantiquement requis".
Le problème avec null dans les langues qui répètent son erreur est que vous ne pouvez pas dire qu'une fonction attend des données non nullables; c'est fondamentalement inexprimable dans la langue. Cela oblige les fonctions à inclure un grand nombre de plates-formes inutiles de gestion des null, et oublier une vérification nulle est une source courante de bogues.
Pour contourner ce manque fondamental d'expressivité, certaines communautés (par exemple la communauté Scala) n'utilisent pas null. Dans Scala, si vous voulez une valeur nullable de type
T
, vous prenez un argument de typeOption[T]
, et si vous voulez une valeur non nullable vous prenez juste unT
. Si quelqu'un passe un null dans votre code, votre code explosera. Cependant, il est considéré que le code appelant est le code buggy.Option
est un assez bon remplacement pour null, car les modèles courants de gestion des valeurs nulles sont extraits dans des fonctions de bibliothèque comme.map(f)
ou.getOrElse(someDefault)
.la source