pointeurs nuls vs modèle d'objet nul

27

Attribution: cela découle d'une question connexe de P.SE

Mes antécédents sont en C / C ++, mais j'ai beaucoup travaillé en Java et je suis en train de coder C #. En raison de mon expérience en C, la vérification des pointeurs passés et retournés est de seconde main, mais je reconnais que cela biaise mon point de vue.

J'ai récemment vu la mention du modèle d'objet nul où l'idée est qu'un objet est toujours retourné. Le cas normal renvoie l'objet attendu et rempli et le cas d'erreur renvoie un objet vide au lieu d'un pointeur nul. La prémisse étant que la fonction appelante aura toujours une sorte d'objet à accéder et évitera donc les violations de mémoire à accès nul.

  • Alors, quels sont les avantages / inconvénients d'une vérification nulle par rapport à l'utilisation du modèle d'objet nul?

Je peux voir un code d'appel plus propre avec le NOP, mais je peux aussi voir où cela créerait des échecs cachés qui autrement ne seraient pas déclenchés. Je préférerais que mon application échoue (alias une exception) pendant que je la développe plutôt qu'une fuite silencieuse d'erreur dans la nature.

  • Le modèle d'objet nul ne peut-il pas avoir des problèmes similaires à celui de ne pas effectuer de vérification nulle?

Beaucoup d'objets avec lesquels j'ai travaillé contiennent des objets ou des conteneurs qui leur sont propres. Il semble que je devrais avoir un cas spécial pour garantir que tous les conteneurs de l'objet principal avaient leurs propres objets vides. Il semble que cela puisse devenir moche avec plusieurs couches d'imbrication.

Communauté
la source
Pas le "cas d'erreur" mais "dans tous les cas un objet valide".
@ ThorbjørnRavnAndersen - bon point, et j'ai édité la question pour refléter cela. Veuillez me faire savoir si je déforme encore la prémisse de NOP.
6
La réponse courte est que "modèle d'objet nul" est un terme impropre. Il est plus précisément appelé «anti-modèle d'objet nul». Vous êtes généralement mieux avec un "objet d'erreur" - un objet valide qui implémente toute l'interface de la classe, mais toute utilisation de celui-ci provoque une mort soudaine et bruyante.
Jerry Coffin
1
@JerryCoffin ce cas doit être indiqué par une exception. L'idée est que vous ne pouvez jamais obtenir une valeur nulle et que vous n'avez donc pas besoin de vérifier la valeur null.
1
Je n'aime pas ce "motif". Mais il est peut-être enraciné dans des bases de données relationnelles surcontraintes, où il est plus facile de se référer à des "lignes nulles" spéciales dans les clés étrangères lors du transfert des données modifiables, avant la mise à jour finale lorsque toutes les clés étrangères ne sont parfaitement pas nulles.

Réponses:

24

Vous n'utiliseriez pas un modèle d'objet nul dans les endroits où null (ou objet nul) est renvoyé en raison d'une défaillance catastrophique. Dans ces endroits, je continuerais de retourner nul. Dans certains cas, s'il n'y a pas de récupération, vous pouvez aussi bien planter car au moins le vidage sur incident indiquera exactement où le problème s'est produit. Dans de tels cas, lorsque vous ajoutez votre propre gestion des erreurs, vous allez toujours tuer le processus (encore une fois, je l'ai dit pour les cas où il n'y a pas de récupération), mais votre gestion des erreurs masquera des informations très importantes qu'un vidage sur incident aurait fourni.

Null Object Pattern est plus pour les endroits où il y a un comportement par défaut qui pourrait être pris dans le cas où un objet n'est pas trouvé. Par exemple, considérez ce qui suit:

User* pUser = GetUser( "Bob" );

if( pUser )
{
    pUser->SetAddress( "123 Fake St." );
}

Si vous utilisez NOP, vous écririez:

GetUser( "Bob" )->SetAddress( "123 Fake St." );

Notez que le comportement de ce code est "si Bob existe, je veux mettre à jour son adresse". De toute évidence, si votre application nécessite la présence de Bob, vous ne voulez pas réussir en silence. Mais il y a des cas où ce type de comportement serait approprié. Et dans ces cas, NOP ne produit-il pas un code beaucoup plus propre et concis?

Dans les endroits où vous ne pouvez vraiment pas vivre sans Bob, GetUser () lèverait une exception d'application (c'est-à-dire pas une violation d'accès ou quelque chose comme ça) qui serait gérée à un niveau supérieur et signalerait un échec général de l'opération. Dans ce cas, il n'y a pas besoin de NOP mais il n'est pas non plus nécessaire de vérifier explicitement NULL. IMO, ces vérifications pour NULL, ne font qu'agrandir le code et enlèvent la lisibilité. Vérifier NULL est toujours le bon choix de conception pour certaines interfaces, mais pas autant que certaines personnes le pensent.

DXM
la source
4
Acceptez le cas d'utilisation des modèles d'objets nuls. Mais pourquoi retourner nul en cas d'échecs catastrophiques? Pourquoi ne pas lever les exceptions?
Apoorv Khurasia
2
@MonsterTruck: inversez cela. Lorsque j'écris du code, je m'assure de valider la majorité des entrées reçues des composants externes. Cependant, entre les classes internes si j'écris du code tel qu'une fonction d'une classe ne retournera jamais NULL, alors du côté appelant je n'ajouterai pas de vérification nulle juste pour "être plus sûr". La seule façon dont cette valeur pourrait être NULL est si une erreur logique catastrophique s'est produite dans mon application. Dans ce cas, je veux que mon programme tombe en panne exactement à cet endroit car alors j'obtiens un joli fichier de vidage sur incident et inverse la pile et l'état de l'objet pour identifier l'erreur logique.
DXM
2
@MonsterTruck: par "inverser cela", je voulais dire, ne renvoie pas explicitement NULL lorsque vous identifiez une erreur catastrophique, mais attendez-vous à ce que NULL se produise uniquement en raison d'une erreur catastrophique imprévue.
DXM
Je comprends maintenant ce que vous entendiez par erreur catastrophique - quelque chose qui provoque l'échec de l'allocation d'objet elle-même. Droite? Edit: impossible de faire @ DXM car votre pseudo ne fait que 3 caractères.
Apoorv Khurasia
@MonsterTruck: bizarre à propos de "@" chose. Je sais que d'autres personnes l'ont déjà fait. Je me demande s'ils ont modifié le site Web récemment. Il n'est pas nécessaire que ce soit une allocation. Si j'écris une classe et que l'intention de l'API est de toujours renvoyer un objet valide, je ne demanderais pas à l'appelant d'effectuer une vérification nulle. Mais une erreur catastrophique pourrait être quelque chose comme une erreur de programmation (une faute de frappe qui a renvoyé NULL), ou peut-être une condition de concurrence qui a effacé un objet avant son heure, ou peut-être une corruption de pile. Essentiellement, tout chemin de code inattendu qui a conduit au retour de NULL. Lorsque cela se produit, vous voulez ...
DXM
7

Alors, quels sont les avantages / inconvénients d'une vérification nulle par rapport à l'utilisation du modèle d'objet nul?

Avantages

  • Une vérification nulle est meilleure car elle résout plus de cas. Tous les objets n'ont pas un comportement sain par défaut ou sans opération.
  • Une vérification nulle est plus solide. Même les objets avec des valeurs par défaut saines sont utilisés dans des endroits où la valeur par défaut saine n'est pas valide. Le code devrait échouer près de la cause racine s'il échoue. Le code devrait évidemment échouer s'il va échouer.

Les inconvénients

  • Des valeurs par défaut raisonnables entraînent généralement un code plus propre.
  • Les défauts sains entraînent généralement des erreurs moins catastrophiques s'ils parviennent à entrer dans la nature.

Ce dernier "pro" est le principal facteur de différenciation (d'après mon expérience) quant au moment où chacun doit être appliqué. "L'échec devrait-il être bruyant?". Dans certains cas, vous voulez qu'un échec soit dur et immédiat; si un scénario qui ne devrait jamais se produire arrive. Si une ressource vitale n'a pas été trouvée ... etc. Dans certains cas, vous êtes d'accord avec un défaut sain car ce n'est pas vraiment une erreur : obtenir une valeur à partir d'un dictionnaire, mais la clé est manquante par exemple.

Comme toute autre décision de conception, il y a des avantages et des inconvénients selon vos besoins.

Telastyn
la source
1
Dans la section Pros: d'accord avec le premier point. Le deuxième point est la faute du concepteur car même null peut être utilisé là où il ne devrait pas l'être. Ne dit rien de mal à propos du modèle, ne fait que réfléchir le développeur qui a mal utilisé le modèle.
Apoorv Khurasia
Quoi de plus échec catstrophique, ne pas pouvoir envoyer d'argent ou envoyer (et donc ne plus avoir) d'argent sans que le destinataire les reçoive et ne le sache pas avant le contrôle explicite?
user470365
Inconvénients: les valeurs par défaut Sane peuvent être utilisées pour construire de nouveaux objets. Si un objet non nul est renvoyé, certains de ses attributs peuvent être modifiés lors de la création d'un autre objet. Si un objet nul est retourné, il peut être utilisé pour créer un nouvel objet. Dans les deux cas, le même code fonctionne. Pas besoin de code séparé pour copier-modifier et nouveau. Cela fait partie de la philosophie selon laquelle chaque ligne de code est un autre endroit pour les bogues; réduire les lignes réduit les bugs.
shawnhcorey
@shawnhcorey - quoi?
Telastyn
Un objet nul devrait être utilisable comme s'il s'agissait d'un objet réel. Par exemple, considérons le NaN de l'IEEE (pas un nombre). Si une équation tourne mal, elle est retournée. Et il peut être utilisé pour d'autres calculs car toute opération sur celui-ci retournera NaN. Il simplifie le code car vous n'avez pas à vérifier NaN après chaque opération arithmétique.
shawnhcorey
6

Je ne suis pas une bonne source d'informations objectives, mais subjectivement:

  • Je ne veux pas que mon système meure, jamais; pour moi, c'est le signe d'un système mal conçu ou incomplet; le système complet doit gérer tous les cas possibles et ne jamais entrer dans un état inattendu; pour y parvenir, je dois être explicite dans la modélisation de tous les flux et situations; l'utilisation de null n'est pas explicite et regroupe beaucoup de cas différents sous une seule umrella; Je préfère l'objet NonExistingUser à null, je préfère NaN à null, ... une telle approche me permet d'être explicite sur ces situations et leur gestion avec autant de détails que je veux, tandis que null ne me laisse qu'une option nulle; dans un tel environnement comme Java, tout objet peut être nul, alors pourquoi préféreriez-vous cacher dans ce pool générique de cas également votre cas spécifique?
  • les implémentations nulles ont un comportement radicalement différent de l'objet et sont pour la plupart collées à l'ensemble de tous les objets (pourquoi? pourquoi? pourquoi?); pour moi, une telle différence drastique semble être le résultat de la conception de null pour indiquer une erreur dans quel cas le système mourra probablement (je suis surpris que beaucoup de gens préfèrent réellement que leur système meure dans les messages ci-dessus) sauf si explicitement traité; pour moi, c'est une approche défectueuse dans sa racine - elle permet au système de mourir par défaut à moins que vous ne vous en soyez explicitement occupé, ce n'est pas très sûr, non? mais dans tous les cas, il est impossible d'écrire un code qui traite la valeur null et object de la même manière - seuls quelques opérateurs intégrés de base (assign, equal, param, etc.) fonctionneront, tous les autres codes échoueront et vous aurez besoin deux voies complètement différentes;
  • mieux utiliser les échelles d'objets nuls - vous pouvez y mettre autant d'informations et de structure que vous le souhaitez; NonExistingUser est un très bon exemple - il peut contenir un e-mail de l'utilisateur que vous avez essayé de recevoir et suggérer de créer un nouvel utilisateur sur la base de ces informations, tandis qu'avec la solution nulle, je devrais réfléchir à la façon de conserver l'e-mail auquel les gens ont tenté d'accéder. proche de la gestion des résultats nuls;

Dans un environnement comme Java ou C #, cela ne vous donnera pas le principal avantage d'explicitness - la sécurité de la solution étant complète, car vous pouvez toujours recevoir null à la place de tout objet dans le système et Java n'a aucun moyen (sauf les annotations personnalisées pour Beans) , etc.) pour vous en empêcher, à l'exception des if explicites. Mais dans le système exempt d'implémentations nulles, l'approche de la solution du problème de cette manière (avec des objets représentant des cas exceptionnels) offre tous les avantages mentionnés. Essayez donc de lire autre chose que les langages traditionnels et vous vous retrouverez à changer vos méthodes de codage.

En général, les objets Null / Error / types de retour sont considérés comme une stratégie de gestion des erreurs très solide et assez mathématiquement solide, tandis que la stratégie de gestion des exceptions de Java ou C va comme une étape au-dessus de la stratégie "die ASAP" - donc laissez fondamentalement toute la charge de instabilité et imprévisibilité du système sur le développeur ou le mainteneur.

Il existe également des monades qui peuvent être considérées comme supérieures à cette stratégie (également supérieures dans la complexité de la compréhension au début), mais celles-ci concernent davantage les cas où vous devez modéliser des aspects externes de type, tandis que Null Object modélise des aspects internes. Donc NonExistingUser est probablement mieux modélisé comme monade peut-être_existant.

Et enfin, je ne pense pas qu'il existe un lien entre le modèle Null Object et la gestion silencieuse des situations d'erreur. Cela devrait être l'inverse - cela devrait vous obliger à modéliser chaque cas d'erreur de manière explicite et à le traiter en conséquence. Maintenant, bien sûr, vous pourriez décider d'être bâclé (pour quelque raison que ce soit) et d'ignorer la gestion explicite du cas d'erreur, mais de le gérer de manière générique - eh bien, c'était votre choix explicite.

Bohdan Tsymbala
la source
9
La raison pour laquelle j'aime que ma demande échoue quand quelque chose d'inattendu se produit, c'est que quelque chose d'inattendu s'est produit. Comment est-ce arrivé? Pourquoi? J'obtiens un vidage sur incident et je peux enquêter et résoudre un problème potentiellement dangereux, plutôt que de le laisser se cacher dans le code. En C #, je peux bien sûr intercepter toutes les exceptions inattendues pour essayer de récupérer l'application, mais même dans ce cas, je veux enregistrer l'endroit où le problème s'est produit et savoir si c'est quelque chose qui nécessite une gestion distincte (même si c'est "ne pas" consigner cette erreur, elle est en fait attendue et récupérable ").
Luaan
3

Je pense qu'il est intéressant de noter que la viabilité d'un objet nul semble être un peu différente selon que votre langue est ou non typée statiquement. Ruby est un langage qui implémente un objet pour Null (ou Nildans la terminologie du langage).

Au lieu de renvoyer un pointeur vers la mémoire non initialisée, la langue renvoie l'objet Nil. Ceci est facilité par le fait que la langue est typée dynamiquement. Il n'est pas garanti qu'une fonction renvoie toujours un int. Il peut revenir Nilsi c'est ce qu'il doit faire. Cela devient plus compliqué et difficile de le faire dans un langage de type statique, car vous vous attendez naturellement à ce que null soit applicable à tout objet pouvant être appelé par référence. Vous devez commencer à implémenter des versions nulles de tout objet qui pourrait être nul (ou suivre la route Scala / Haskell et avoir une sorte de wrapper "Peut-être").

MrLister a évoqué le fait qu'en C ++, il n'est pas garanti d'avoir une référence / un pointeur d'initialisation lors de sa première création. En ayant un objet null dédié au lieu d'un pointeur null brut, vous pouvez obtenir de belles fonctionnalités supplémentaires. Ruby Nilcomprend une fonction to_s(toString) qui se traduit par du texte vierge. C'est très bien si cela ne vous dérange pas d'obtenir un retour nul sur une chaîne et que vous souhaitez ignorer le signalement sans planter. Les classes ouvertes vous permettent également de remplacer manuellement la sortie de Nilsi besoin est.

Certes, tout cela peut être pris avec un grain de sel. Je crois que dans un langage dynamique, l'objet nul peut être très pratique lorsqu'il est utilisé correctement. Malheureusement, pour en tirer le meilleur parti, vous devrez peut-être modifier ses fonctionnalités de base, ce qui pourrait rendre votre programme difficile à comprendre et à maintenir pour les personnes habituées à travailler avec ses formulaires plus standard.

KChaloux
la source
1

Pour un point de vue supplémentaire, jetez un œil à Objective-C, où au lieu d'avoir un objet Null, le type de valeur nil fonctionne comme un objet. Tout message envoyé à un objet nil n'a aucun effet et renvoie une valeur de 0 / 0,0 / NO (faux) / NULL / nil, quel que soit le type de valeur de retour de la méthode. Ceci est utilisé comme un modèle très omniprésent dans la programmation MacOS X et iOS, et généralement les méthodes seront conçues pour qu'un objet nul retournant 0 soit un résultat raisonnable.

Par exemple, si vous souhaitez vérifier si une chaîne contient un ou plusieurs caractères, vous pouvez écrire

aString.length > 0

et obtenir un résultat raisonnable si aString == nil, car une chaîne inexistante ne contient aucun caractère. Les gens ont tendance à prendre un certain temps pour s'y habituer, mais il me faudrait probablement un certain temps pour m'habituer à vérifier les valeurs nulles partout dans d'autres langues.

(Il existe également un objet singleton de classe NSNull qui se comporte assez différemment et n'est pas très utilisé en pratique).

gnasher729
la source
Objective-C a rendu la chose Null Object / Null Pointer vraiment floue. nilget #define'd à NULL et se comporte comme un objet nul. Il s'agit d'une fonctionnalité d'exécution alors qu'en C # / Java, les pointeurs nuls émettent des erreurs.
Maxthon Chan
0

N'utilisez pas non plus si vous pouvez l'éviter. Utilisez une fonction d'usine renvoyant un type d'option, un tuple ou un type de somme dédié. En d'autres termes, représentent une valeur potentiellement par défaut avec un type différent d'une valeur garantie par défaut.

D'abord, listons les desiderata puis réfléchissons à la façon dont nous pouvons l'exprimer dans quelques langages (C ++, OCaml, Python)

  1. attraper l'utilisation non désirée d'un objet par défaut au moment de la compilation.
  2. indiquez clairement si une valeur donnée est potentiellement par défaut ou non lors de la lecture du code.
  3. choisissez notre valeur par défaut sensible, le cas échéant, une fois par type . Ne choisissez certainement pas un défaut potentiellement différent sur chaque site d'appel .
  4. le rendre facile pour les outils d'analyse statique ou un humain avec greppour rechercher des erreurs potentielles.
  5. Pour certaines applications , le programme doit continuer normalement s'il reçoit une valeur par défaut de manière inattendue. Pour d'autres applications , le programme doit s'arrêter immédiatement s'il reçoit une valeur par défaut, idéalement de manière informative.

Je pense que la tension entre le modèle d'objet nul et les pointeurs nuls vient de (5). Si nous pouvons détecter les erreurs assez tôt, cependant, (5) devient théorique.

Considérons cette langue par langue:

C ++

À mon avis, une classe C ++ devrait généralement être constructible par défaut car elle facilite les interactions avec les bibliothèques et facilite l'utilisation de la classe dans les conteneurs. Cela simplifie également l'héritage car vous n'avez pas besoin de penser au constructeur de superclasse à appeler.

Cependant, cela signifie que vous ne pouvez pas savoir avec certitude si une valeur de type MyClassest dans "l'état par défaut" ou non. En plus de mettre un bool nonemptychamp ou un champ similaire pour faire apparaître la valeur par défaut lors de l'exécution, le mieux que vous puissiez faire est de produire de nouvelles instances de MyClassmanière à obliger l'utilisateur à le vérifier .

Je recommanderais d'utiliser une fonction d'usine qui renvoie un std::optional<MyClass>, std::pair<bool, MyClass>ou une référence de valeur r à un std::unique_ptr<MyClass>si possible.

Si vous voulez que votre fonction d'usine retourne une sorte d'état "d'espace réservé" qui est différent d'un construit par défaut MyClass, utilisez un std::pairet assurez-vous de documenter que votre fonction fait cela.

Si la fonction d'usine a un nom unique, il est facile de greprechercher le nom et de rechercher la négligence. Cependant, il est difficile de trouver grepdes cas où le programmeur aurait dû utiliser la fonction d'usine mais ne l'a pas fait .

OCaml

Si vous utilisez un langage comme OCaml, vous pouvez simplement utiliser un optiontype (dans la bibliothèque standard OCaml) ou un eithertype (pas dans la bibliothèque standard, mais facile à rouler le vôtre). Ou un defaultabletype (j'invente le terme defaultable).

type 'a option =
    | None
    | Some of 'a

type ('a, 'b) either =
    | Left of 'a
    | Right of 'b

type 'a defaultable =
    | Default of 'a
    | Real of 'a

Un defaultablecomme indiqué ci-dessus est meilleur qu'une paire car l'utilisateur doit correspondre à un modèle pour extraire le 'aet ne peut pas simplement ignorer le premier élément de la paire.

L'équivalent du defaultabletype indiqué ci-dessus peut être utilisé en C ++ en utilisant un std::variantavec deux instances du même type, mais des parties de l' std::variantAPI sont inutilisables si les deux types sont identiques. C'est aussi une utilisation étrange std::variantcar ses constructeurs de types sont sans nom.

Python

De toute façon, vous n'obtenez aucune vérification à la compilation pour Python. Mais, étant typé dynamiquement, il n'y a généralement pas de circonstances où vous avez besoin d'une instance d'espace réservé d'un certain type pour apaiser le compilateur.

Je recommanderais simplement de lever une exception lorsque vous seriez obligé de créer une instance par défaut.

Si ce n'est pas acceptable, je recommanderais de créer un DefaultedMyClassqui hérite MyClasset de le renvoyer de votre fonction d'usine. Cela vous offre de la flexibilité en termes de fonctionnalité de "stubbing out" dans les instances par défaut si vous en avez besoin.

Gregory Nisbet
la source