Un nouveau champ booléen est-il préférable à une référence nulle lorsqu'une valeur peut être absente de manière significative?

39

Par exemple, supposons que j'ai une classe Member, qui a un lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

dont lastChangePasswordTime peut être absent de manière significative, car certains membres peuvent ne jamais changer leurs mots de passe.

Mais selon Si les valeurs nulles sont mauvaises, que faut-il utiliser lorsqu'une valeur peut être absente de manière significative? et https://softwareengineering.stackexchange.com/a/12836/248528 , je ne devrais pas utiliser null pour représenter une valeur significativement absente. Alors j'essaie d'ajouter un drapeau booléen:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Mais je pense que c'est assez obsolète parce que:

  1. Lorsque isPasswordChanged a la valeur false, lastChangePasswordTime doit être null et la vérification de lastChangePasswordTime == null est presque identique à la vérification que isPasswordChanged est false, je préfère donc vérifier lastChangePasswordTime == null directement

  2. En changeant la logique ici, je peux oublier de mettre à jour les deux champs.

Remarque: lorsqu'un utilisateur change de mot de passe, j'enregistre l'heure comme ceci:

this.lastChangePasswordTime=Date.now();

Le champ booléen supplémentaire est-il meilleur qu'une référence nulle ici?

ocomfd
la source
51
Cette question est assez spécifique à la langue, car ce qui constitue une bonne solution dépend en grande partie de ce que propose votre langage de programmation. Par exemple, en C ++ 17 ou Scala, vous utiliseriez std::optionalou Option. Dans d’autres langues, vous devrez peut-être créer vous-même un mécanisme approprié, ou bien vous pourrez recourir à nullquelque chose de similaire, car il est plus idiomatique.
Christian Hackl
16
Y a-t-il une raison pour laquelle vous ne voudriez pas que lastChangePasswordTime soit défini sur l'heure de création du mot de passe (la création est un mod, après tout)?
Kristian H
@ChristianHackl hmm, convenez qu'il existe différentes solutions "parfaites", mais je ne vois pas de langage (majeur) bien que l'utilisation d'un booléen distinct serait une meilleure idée en général que de faire des contrôles null / nil. Pas tout à fait sûr de C / C ++ car je n'y suis pas actif depuis un bon moment.
Frank Hopkins
@FrankHopkins: Un exemple serait les langages dans lesquels les variables peuvent être laissées non initialisées, par exemple C ou C ++. lastChangePasswordTimepeut être un pointeur non initialisé ici, et le comparer à quoi que ce soit serait un comportement indéfini. Ce n'est pas une raison vraiment convaincante de ne pas initialiser le pointeur sur NULL/ à la nullptrplace, surtout pas en C ++ moderne (où vous n'utiliseriez pas de pointeur du tout), mais qui sait? Un autre exemple serait les langues sans pointeurs, ou avec un mauvais support pour les pointeurs, peut-être. (FORTRAN 77 me vient à l'esprit ...)
Christian Hackl le
J'utiliserais une énumération avec 3 cas pour cela :)
J. Doe

Réponses:

72

Je ne vois pas pourquoi, si vous avez une valeur absente de manière significative, nullne devrait pas être utilisé si vous êtes délibéré et prudent à ce sujet.

Si votre objectif est d’entourer la valeur nullable afin d’éviter de la référencer accidentellement, nous vous suggérons de créer la isPasswordChangedvaleur en tant que fonction ou propriété renvoyant le résultat d’un contrôle nul, par exemple:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

À mon avis, le faire de cette façon:

  • Donne une meilleure lisibilité du code qu'une vérification nulle, ce qui pourrait perdre du contexte.
  • isPasswordChangedCela vous évite d'avoir à vous soucier de maintenir la valeur que vous mentionnez.

La façon dont vous conservez les données (probablement dans une base de données) serait responsable de la préservation des valeurs NULL.

TZHX
la source
29
+1 pour l'ouverture. L'utilisation gratuite de null est généralement refusée uniquement dans les cas où null est un résultat inattendu, c'est-à-dire où cela n'aurait pas de sens (pour le consommateur). Si null est significatif, alors ce n'est pas un résultat inattendu.
Flater
28
Je dirais un peu plus fort que jamais deux variables qui maintiennent le même état. Ça va échouer. Cacher une valeur nulle, en supposant que la valeur nulle ait un sens à l'intérieur de l'instance, de l'extérieur est une très bonne chose. Dans ce cas, vous pourrez décider plus tard que 1970-01-01 indique que le mot de passe n'a jamais été modifié. Ensuite, la logique du reste du programme ne doit pas être prise en compte.
Bent
3
Vous pouvez oublier d'appeler isPasswordChangedtout comme vous pouvez oublier de vérifier si la valeur est nulle. Je ne vois rien gagné ici.
Gherman le
9
@Gherman Si le consommateur n'obtient pas le concept que null est significatif ou qu'il a besoin de vérifier la valeur présente, il est perdu de toute façon, mais si la méthode est utilisée, il est clair pour tout le monde qui lit le code pourquoi il est un contrôle et qu'il existe une chance raisonnable que la valeur soit null / absent. Autrement, il est difficile de savoir s'il s'agissait simplement d'un développeur qui ajoute un contrôle nul simplement "parce que pourquoi pas" ou s'il fait partie du concept. Bien sûr, on peut le savoir, mais d’une manière vous avez l’information directement de l’autre, vous avez besoin de regarder dans la mise en œuvre.
Frank Hopkins le
2
@ FrankHopkins: C'est un bon point, mais si l'accès simultané est utilisé, même l'inspection d'une seule variable peut nécessiter un verrouillage.
Christian Hackl le
63

nullsne sont pas mauvais. Les utiliser sans réfléchir est. C'est un cas où nullla réponse est exacte - il n'y a pas de date.

Notez que votre solution crée plus de problèmes. Quel est le sens de la date étant mise à quelque chose, mais le isPasswordChangedest faux? Vous venez de créer un cas d'informations conflictuelles que vous devez capturer et traiter spécialement, alors qu'une nullvaleur a un sens clairement défini et non ambigu et ne peut pas être en conflit avec d'autres informations.

Alors non, votre solution n'est pas meilleure. Permettre une nullvaleur est la bonne approche ici. Les gens qui prétendent que nullc'est toujours mauvais, peu importe le contexte, ne comprennent pas pourquoi nullexiste.

À M
la source
17
Historiquement, cela nullexiste parce que Tony Hoare a commis l’erreur de 1 milliard de dollars en 1965. Les gens qui prétendent que cela nullest "pervers" simplifient excessivement le sujet, bien sûr, mais c’est une bonne chose que les langages modernes ou les styles de programmation changent peu à peu null. il avec des types d'option dédiés.
Christian Hackl
9
@ChristianHackl: juste par intérêt historique - Hoare a-t-il jamais dit quelle aurait été la meilleure alternative pratique (sans passer à un tout nouveau paradigme)?
AnoE
5
@AnoE: Question intéressante. Je ne sais pas ce que Hoare avait à dire à ce sujet, mais la programmation sans zéro est souvent une réalité de nos jours dans des langages de programmation modernes et bien conçus.
Christian Hackl
10
@ Tom Wat? Dans des langages tels que C # et Java, le DateTimechamp est un pointeur. Le concept entier de nulln'a de sens que pour les pointeurs!
Todd Sewell
16
@Tom: Les pointeurs nuls ne sont dangereux que pour les langages et les implémentations qui leur permettent d'être indexés aveuglément et déréférencés, quelle que soit l'hilarité qui puisse en résulter. Dans un langage qui intercepte les tentatives d'indexer ou de déréférencer un pointeur nul, ce sera souvent moins dangereux qu'un pointeur vers un objet factice. Dans de nombreuses situations, il peut être préférable d’avoir un programme qui se termine anormalement, plutôt que de le laisser produire des nombres sans signification, et sur les systèmes qui les interceptent, les pointeurs nuls sont la bonne recette pour cela.
Supercat
29

Selon votre langage de programmation, il peut exister de bonnes alternatives, telles qu'un optionaltype de données. En C ++ (17), ce serait std :: optional . Il peut être absent ou avoir une valeur arbitraire du type de données sous-jacent.

dasmy
la source
8
Il y a aussi Optionalen java; le sens d'un nul Optionalétant à vous ...
Matthieu M.
1
Entré ici pour se connecter avec facultatif aussi. Je l'encoderais comme un type dans une langue qui les avait.
Zipp
2
@ FrankHopkins C'est dommage que rien en Java ne vous empêche d'écrire Optional<Date> lastPasswordChangeTime = null;, mais je n'abandonnerais pas à cause de cela. Au lieu de cela, je voudrais instiller une règle d'équipe très fermement, qu'aucune valeur optionnelle ne peut jamais être assignée null. En option, vous achetez trop de fonctionnalités intéressantes pour pouvoir y renoncer facilement. Vous pouvez facilement attribuer des valeurs par défaut ( orElseou avec une évaluation paresseuse:) orElseGet, des erreurs de projection ( orElseThrow), transformer des valeurs de manière sécurisée ( map, flatmap), etc.
Alexander - Rétablir Monica
5
@FrankHopkins La vérification isPresentest presque toujours une odeur de code, selon mon expérience, et un signe assez fiable que les développeurs qui utilisent ces options sont "manquants". En effet, cela n’apporte fondamentalement aucun avantage ref == null, mais ce n’est pas quelque chose que vous devriez utiliser souvent. Même si l'équipe est instable, un outil d'analyse statique peut énoncer la loi, comme un linter ou un FindBugs , qui peut intercepter la plupart des cas.
Alexander - Réintégrer Monica le
5
@ FrankHopkins: Il se peut que la communauté Java ait juste besoin d'un petit changement de paradigme, ce qui peut prendre plusieurs années. Si vous regardez Scala, par exemple, il prend en charge nullparce que la langue est 100% compatible avec Java, mais personne ne l' utilise, et Option[T]est partout, avec un excellent support fonctionnel programmation comme map, flatMap, correspondance de motif et ainsi de suite, qui va loin, bien au - delà des == nullvérifications. Vous avez raison de dire qu'en théorie, un type d'option ne ressemble guère à un pointeur glorifié, mais la réalité prouve que cet argument est faux.
Christian Hackl
11

Utiliser correctement null

Il y a différentes façons d'utiliser null. La manière la plus courante et sémantiquement correcte consiste à l'utiliser lorsque vous pouvez avoir ou non une valeur unique . Dans ce cas, une valeur est égale nullou significative (par exemple, un enregistrement de base de données).

Dans ces situations, vous l'utilisez principalement comme ceci (en pseudo-code):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

Problème

Et c'est un très gros problème. Le problème est qu’au moment où vous appelez, doSomethingUsefulla valeur n’a peut-être pas été vérifiée null! Si ce n'était pas le cas, le programme va probablement planter. Et l'utilisateur peut même ne pas voir de bons messages d'erreur, laissant quelque chose comme "une erreur horrible: valeur recherchée mais nulle"! " (après la mise à jour: bien qu'il puisse y avoir encore moins d'erreur informative comme Segmentation fault. Core dumped., ou pire encore, aucune erreur et une manipulation incorrecte sur null dans certains cas)

Oublier d'écrire des chèques nullet de gérer des nullsituations est un bogue extrêmement courant . Voilà pourquoi Tony Hoare qui a inventé nulllors d' une conférence de logiciel appelé QCon Londres en 2009 qu'en 1965 , il a fait l'erreur d'un milliard de dollars: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Erreur-Tony-Hoare

Éviter le problème

Certaines technologies et langues rendent la vérification nullimpossible à oublier de différentes manières, réduisant ainsi le nombre de bugs.

Par exemple, Haskell a la Maybemonade au lieu de NULL. Supposons qu'il s'agisse d' DatabaseRecordun type défini par l'utilisateur. Dans Haskell, une valeur de type Maybe DatabaseRecordpeut être égale Just <somevalue>ou égale à Nothing. Vous pouvez ensuite l'utiliser de différentes manières, mais vous ne pouvez pas appliquer une opération Nothingsans le savoir, peu importe la façon dont vous l'utilisez .

Par exemple, cette fonction appelée zeroAsDefaultrenvoie xpour Just xet 0pour Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl dit que C ++ 17 et Scala ont leurs propres méthodes. Donc, vous voudrez peut-être essayer de savoir si votre langue a quelque chose comme ça et l'utiliser.

Les null sont encore largement utilisés

Si vous n'avez rien de mieux à utiliser, alors nullc'est bien. Continuez juste à le surveiller. Les déclarations de type dans les fonctions vous aideront quand même un peu.

En outre, cela peut sembler peu progressif, mais vous devriez vérifier si vos collègues veulent utiliser nullquelque chose d’autre. Ils peuvent être conservateurs et ne pas vouloir utiliser de nouvelles structures de données pour certaines raisons. Par exemple, supporter les anciennes versions d'une langue. Ces éléments doivent être déclarés dans les normes de codage du projet et dûment discutés avec l'équipe.

Sur votre proposition

Vous suggérez d'utiliser un champ booléen séparé. Mais vous devez quand même le vérifier et vous pouvez toujours oublier de le vérifier. Donc, il n'y a rien gagné ici. Si vous pouvez même oublier autre chose, comme mettre à jour les deux valeurs à chaque fois, c'est encore pire. Si le problème de l’oubli de la vérification nulln’est pas résolu, cela ne sert à rien. Éviter nullest difficile et vous ne devriez pas le faire de manière à aggraver les choses.

Comment ne pas utiliser null

Enfin, il existe des moyens courants d'utiliser de nullmanière incorrecte. L’un de ces moyens consiste à l’utiliser à la place de structures de données vides telles que des tableaux et des chaînes. Un tableau vide est un tableau approprié comme un autre! Il est presque toujours important et utile que les structures de données, qui peuvent contenir plusieurs valeurs, puissent être vides, c'est-à-dire qu'elles ont une longueur égale à 0.

Du point de vue de l'algèbre, une chaîne vide pour les chaînes ressemble beaucoup à 0 pour les nombres, c'est-à-dire l'identité:

a+0=a
concat(str, '')=str

Une chaîne vide permet aux chaînes de devenir en général un monoïde: https://en.wikipedia.org/wiki/Monoid Si vous ne l'obtenez pas, ce n'est pas si important pour vous.

Voyons maintenant pourquoi il est important de programmer avec cet exemple:

for (element in array) {
  doSomething(element);
}

Si nous passons ici un tableau vide, le code fonctionnera correctement. Cela ne fera que rien. Cependant, si nous passons un nullici, nous aurons probablement un crash avec une erreur du type "impossible de parcourir en boucle null, désolé". Nous pourrions l'envelopper ifmais c'est moins propre et encore une fois, vous pourriez oublier de vérifier

Comment gérer null

Ce qui doSomethingAboutIt()devrait être fait et surtout s’il faut lever une exception est une autre question compliquée. En bref, cela dépend de la question de savoir si nullétait une valeur d'entrée acceptable pour une tâche donnée et ce qui est attendu en réponse. Les exceptions concernent des événements inattendus. Je n'irai pas plus loin dans ce sujet. Cette réponse est déjà très longue.

Gherman
la source
5
En fait, horrible error: wanted value but got null!c'est beaucoup mieux que le plus typique Segmentation fault. Core dumped....
Toby Speight
2
@TobySpeight C'est vrai, mais je préférerais quelque chose qui reste dans les termes de l'utilisateur, comme Error: the product you want to buy is out of stock.
Gherman le
Bien sûr, je faisais juste remarquer que cela pourrait être (et est souvent) encore pire . Je suis complètement d'accord sur ce qui serait mieux! Et votre réponse est à peu près ce que j'aurais dit à cette question: +1.
Toby Speight le
2
@Gherman: Passer nullnulln'est pas autorisé est une erreur de programmation, c'est-à-dire un bug dans le code. Un produit en rupture de stock fait partie de la logique métier ordinaire et n'est pas un bug. Vous ne pouvez pas et ne devez pas essayer de traduire la détection d’un bogue en un message normal dans l’interface utilisateur. Il est préférable d’intervenir immédiatement et de ne pas exécuter plus de code, surtout s’il s’agit d’une application importante (par exemple, une application qui effectue de véritables transactions financières).
Christian Hackl
1
@EricDuminil Nous voulons supprimer (ou réduire) la possibilité de commettre une erreur, et non pas supprimer nullentièrement, même de l'arrière-plan.
Gherman le
4

En plus de toutes les très bonnes réponses données précédemment, j'ajouterais que chaque fois que vous êtes tenté de laisser un champ nul, réfléchissez bien s'il peut s'agir d'un type de liste. Un type nullable est équivalent à une liste de 0 ou 1 éléments, ce qui peut souvent être généralisé à une liste de N éléments. Plus précisément dans ce cas, vous pouvez envisager de lastChangePasswordTimeconstituer une liste de passwordChangeTimes.

Joel
la source
C'est un excellent point. La même chose est souvent vraie pour les types d'options; une option peut être considérée comme un cas particulier d'une séquence.
Christian Hackl
2

Posez-vous la question suivante: quel comportement requiert le champ lastChangePasswordTime?

Si vous avez besoin de ce champ pour une méthode IsPasswordExpired () afin de déterminer si un membre doit être invité à changer son mot de passe de temps en temps, je définirais le champ à la date à laquelle le membre a été créé. L'implémentation IsPasswordExpired () est la même pour les membres nouveaux et existants.

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Si vous avez une exigence distincte que les membres nouvellement créés doivent mettre à jour leur mot de passe, je voudrais ajouter un champ booléen appelé séparés passwordShouldBeChanged et à true lors de la création. Je voudrais ensuite changer les fonctionnalités de la méthode IsPasswordExpired () pour inclure une vérification de ce champ (et renommer la méthode en ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Rendez vos intentions explicites dans le code.

Rik D
la source
2

Tout d’abord, le fait que le mal soit nul est un dogme et, comme d’habitude avec un dogme, il fonctionne mieux comme ligne directrice que comme test de réussite ou non.

Deuxièmement, vous pouvez redéfinir votre situation de manière à ce que la valeur ne puisse jamais être nulle. InititialPasswordChanged est un booléen initialement défini sur false, PasswordSetTime est la date et l'heure auxquelles le mot de passe actuel a été défini.

Notez que bien que cela vienne à un léger coût, vous pouvez maintenant TOUJOURS calculer le temps écoulé depuis la dernière définition d’un mot de passe.

Jmoreno
la source
1

Les deux sont «sûrs / sains / corrects» si l'appelant vérifie avant l'utilisation. Le problème est ce qui se passe si l'appelant ne vérifie pas. Quel est le meilleur, une sorte d'erreur de null ou d'utiliser une valeur invalide?

Il n'y a pas une seule bonne réponse. Cela dépend de ce qui vous inquiète.

Si les plantages sont vraiment graves mais que la réponse n'est pas critique ou a une valeur par défaut acceptée, il est peut-être préférable d'utiliser un booléen comme indicateur. Si utiliser la mauvaise réponse est un problème pire que de planter, utiliser null est préférable.

Pour la majorité des cas "typiques", un échec rapide et le fait de forcer les appelants à vérifier est le moyen le plus rapide d'obtenir du code sain d'esprit, et par conséquent, je pense que null devrait être le choix par défaut. Je ne mettrais pas trop de foi dans le "X est la racine de tout le mal" évangélistes cependant; ils n'ont normalement pas anticipé tous les cas d'utilisation.

Une
la source