web-dev-qa-db-fra.com

Est-il préférable de retourner des valeurs NULL ou vides à partir de fonctions / méthodes où la valeur de retour n'est pas présente?

Je cherche une recommandation ici. Je me demande s'il est préférable de retourner NULL ou une valeur vide à partir d'une méthode lorsque la valeur de retour n'est pas présente ou ne peut pas être déterminée.

Prenez les deux méthodes suivantes comme exemples:

string ReverseString(string stringToReverse) // takes a string and reverses it.
Person FindPerson(int personID)    // finds a Person with a matching personID.

Dans ReverseString(), je dirais retourner une chaîne vide parce que le type de retour est une chaîne, donc l'appelant attend cela. De cette façon, l'appelant n'aurait pas à vérifier si un NULL a été renvoyé.

Dans FindPerson(), retourner NULL semble être un meilleur ajustement. Indépendamment du fait que NULL ou un objet personne vide (new Person()) soit renvoyé, l'appelant devra vérifier si l'objet personne est NULL ou vide avant de faire quoi que ce soit (comme appeler UpdateName()). Alors pourquoi ne pas simplement retourner NULL ici et alors l'appelant n'a qu'à vérifier NULL.

Est-ce que quelqu'un d'autre a du mal avec ça? Toute aide ou compréhension est appréciée.

145
P B

StackOverflow a une bonne discussion sur ce sujet exact dans ce Q&A . Dans la question la mieux notée, kronoz note:

Renvoyer null est généralement la meilleure idée si vous avez l'intention d'indiquer qu'aucune donnée n'est disponible.

Un objet vide implique que des données ont été retournées, alors que le retour de null indique clairement que rien n'a été retourné.

En outre, le retour d'une valeur Null entraînera une exception Null si vous tentez d'accéder aux membres de l'objet, ce qui peut être utile pour mettre en surbrillance le code bogué - tenter d'accéder à un membre de rien n'a aucun sens. L'accès aux membres d'un objet vide n'échouera pas, ce qui signifie que les bogues peuvent rester inconnus.

Personnellement, j'aime retourner des chaînes vides pour les fonctions qui renvoient des chaînes afin de minimiser la quantité de gestion des erreurs qui doit être mise en place. Cependant, vous devrez vous assurer que le groupe avec lequel vous travaillez suivra la même convention - sinon les avantages de cette décision ne seront pas obtenus.

Cependant, comme l'a noté l'affiche dans la réponse SO, les valeurs nulles devraient probablement être renvoyées si un objet est attendu afin qu'il n'y ait aucun doute sur le retour des données.

En fin de compte, il n'y a pas de meilleure façon de faire les choses. Construire un consensus d'équipe conduira finalement les meilleures pratiques de votre équipe.

81
JW8

Dans tout le code que j'écris, j'évite de renvoyer null depuis une fonction. Je l'ai lu dans Clean Code .

Le problème avec l'utilisation de null est que la personne utilisant l'interface ne sait pas si null est un résultat possible et si elle doit le vérifier, car il n'y a pas de not null Type de référence.

En F #, vous pouvez renvoyer un type option, qui peut être some(Person) ou none, il est donc évident pour l'appelant qu'il doit vérifier.

Le modèle (anti-) C # analogue est la méthode Try...:

public bool TryFindPerson(int personId, out Person result);

Maintenant, je sais que les gens ont dit qu'ils détestent le modèle Try... Car avoir un paramètre de sortie casse les idées d'une fonction pure, mais c'est vraiment pas différent de:

class FindResult<T>
{
   public FindResult(bool found, T result)
   {
       this.Found = found;
       this.Result = result;
   }

   public bool Found { get; private set; }
   // Only valid if Found is true
   public T Result { get; private set;
}

public FindResult<Person> FindPerson(int personId);

... et pour être honnête, vous pouvez supposer que chaque programmeur .NET connaît le modèle Try... car il est utilisé en interne par le. Cadre NET. Cela signifie qu'ils n'ont pas à lire la documentation pour comprendre ce qu'elle fait, ce qui est plus important pour moi que de m'en tenir à la vision d'un puriste des fonctions ( comprendre que result est un paramètre out, pas un paramètre ref).

J'irais donc avec TryFindPerson parce que vous semblez indiquer qu'il est parfaitement normal de ne pas pouvoir le trouver.

Si, d'autre part, il n'y a aucune raison logique que l'appelant fournisse un personId qui n'existait pas, je le ferais probablement:

public Person GetPerson(int personId);

... puis je lèverais une exception si elle n'était pas valide. Le préfixe Get... Implique que l'appelant sait qu'il doit réussir.

85
Scott Whitlock

Vous pouvez essayer le modèle de cas spécial de Martin Fowler , à partir de Paterns of Enterprise Application Architecture :

Les valeurs nulles sont des choses gênantes dans les programmes orientés objet car elles déjouent le polymorphisme. Habituellement, vous pouvez appeler librement foo sur une référence variable d'un type donné sans vous soucier de savoir si l'élément est le type exact ou une sous-classe. Avec un langage fortement typé, vous pouvez même demander au compilateur de vérifier que l'appel est correct. Cependant, comme une variable peut contenir null, vous pouvez rencontrer une erreur d'exécution en invoquant un message sur null, ce qui vous donnera une trace de pile agréable et conviviale.

S'il est possible qu'une variable soit nulle, vous devez vous rappeler de l'entourer de code de test nul afin que vous fassiez ce qu'il faut si un null est présent. Souvent, la bonne chose est la même dans de nombreux contextes, donc vous finissez par écrire du code similaire dans de nombreux endroits - commettant le péché de duplication de code.

Les nuls sont un exemple courant de tels problèmes et d'autres surgissent régulièrement. Dans les systèmes numériques, vous devez faire face à l'infini, qui a des règles spéciales pour des choses comme l'addition qui brisent les invariants habituels des nombres réels. L'une de mes premières expériences dans le domaine des logiciels d'entreprise a été avec un client de service public qui n'était pas entièrement connu, appelé "occupant". Tout cela implique de modifier le comportement habituel du type.

Au lieu de renvoyer null ou une valeur impaire, renvoyez un cas spécial qui a la même interface que ce que l'appelant attend.

31
Thomas Owens

Je penserais que ReverseString() retournerait la chaîne inversée et lancerait un IllegalArgumentException s'il était passé dans un Null.

Je pense que FindPerson() devrait suivre le NullObject Pattern ou lever une exception non vérifiée pour ne pas trouver quelque chose si vous devez toujours être en mesure de trouver quelque chose.

Il faut éviter de traiter avec Null. Avoir Null dans les langues a été appelé Billion Dollar Mistake par son inventeur!

J'appelle cela mon erreur d'un milliard de dollars. C'était l'invention de la référence nulle en 1965. A cette époque, je concevais le premier système de type complet pour les références dans un langage orienté objet (ALGOL W).

Mon objectif était de garantir que toute utilisation des références soit absolument sûre, la vérification étant effectuée automatiquement par le compilateur. Mais je n'ai pas pu résister à la tentation de mettre une référence nulle, tout simplement parce qu'elle était si facile à mettre en œuvre.

Cela a conduit à d'innombrables erreurs, vulnérabilités et plantages du système, qui ont probablement causé un milliard de dollars de douleur et de dommages au cours des quarante dernières années.

Ces dernières années, un certain nombre d'analyseurs de programmes tels que PREfix et PREfast dans Microsoft ont été utilisés pour vérifier les références et donner des avertissements en cas de risque qu'elles ne soient pas nulles. Des langages de programmation plus récents comme Spec # ont introduit des déclarations pour les références non nulles. Telle est la solution que j'ai rejetée en 1965.

Tony Hoare

15
user7519

Renvoie un Option . Tous les avantages de retourner une valeur non valide différente (comme pouvoir avoir des valeurs vides dans vos collections) sans aucun risque d'exception NullPointerException.

13
hugomg

Je vois les deux côtés de cet argument, et je me rends compte que certaines voix plutôt influentes (par exemple, Fowler) préconisent de ne pas renvoyer de valeurs nulles afin de garder le code propre, d'éviter les blocs de gestion des erreurs supplémentaires, etc.

Cependant, j'ai tendance à me ranger du côté des partisans du retour nul. Je trouve qu'il y a une distinction importante dans l'invocation d'une méthode et sa réponse avec je n'ai pas de données et sa réponse avec j'ai cette chaîne vide.

Puisque j'ai vu une partie de la discussion faisant référence à une classe Person, considérez le scénario dans lequel vous essayez de rechercher une instance de la classe. Si vous transmettez un attribut du Finder (par exemple, un ID), un client peut immédiatement vérifier la valeur null pour voir si aucune valeur n'a été trouvée. Ce n'est pas nécessairement exceptionnel (donc pas besoin d'exceptions), mais cela doit également être documenté clairement. Oui, cela nécessite une certaine rigueur de la part du client, et non, je ne pense pas que ce soit une mauvaise chose du tout.

Considérez maintenant l'alternative où vous retournez un objet Person valide ... qui ne contient rien. Mettez-vous des valeurs nulles dans toutes ses valeurs (nom, adresse, favoriteDrink), ou allez-vous maintenant remplir celles-ci avec des objets valides mais vides? Comment votre client peut-il maintenant déterminer qu'aucune personne réelle n'a été trouvée? Doivent-ils vérifier si le nom est une chaîne vide au lieu de null? Ce genre de chose ne va-t-il pas conduire à autant ou plus d'encombrement de code et d'instructions conditionnelles que si nous venions de vérifier la valeur null et de passer à autre chose?

Encore une fois, il y a des points de chaque côté de cet argument avec lesquels je pourrais être d'accord, mais je trouve que cela a le plus de sens pour le plus de personnes (ce qui rend le code plus maintenable).

8
RonU

Renvoyez null si vous avez besoin de savoir si l'article existe ou non. Sinon, retournez le type de données attendu. Cela est particulièrement vrai si vous retournez une liste d'articles. Il est généralement prudent de supposer que si l'appelant veut une liste, il voudra parcourir la liste. De nombreuses langues (la plupart? Toutes?) Échouent si vous essayez d'itérer sur null mais pas lorsque vous parcourez une liste vide.

Je trouve frustrant d'essayer d'utiliser du code comme celui-ci, mais de le faire échouer:

for thing in get_list_of_things() {
    do_something_clever(thing)
}

Je ne devrais pas avoir à ajouter un cas spécial pour le cas où il n'y a pas choses.

6
Bryan Oakley

Cela dépend de la sémantique de la méthode. Vous pouvez éventuellement spécifier que votre méthode accepte uniquement "Non nul". Dans Java vous pouvez déclarer qu'en utilisant des annotations de métadonnées:

string ReverseString(@NotNull String stringToReverse)

Vous devez en quelque sorte spécifier la valeur de retour. Si vous déclarez, vous n'acceptez que des valeurs NotNull, vous êtes tenu de renvoyer une chaîne vide (si l'entrée était également une chaîne vide).

Le deuxième cas est un peu compliqué dans sa sémantique. Si cette méthode consiste à renvoyer une personne par sa clé primaire (et que la personne devrait exister), le meilleur moyen est de lever l'exception.

Person FindPerson(int personID)

Si vous recherchez une personne par un identifiant supposé (ce qui me semble étrange), vous feriez mieux de déclarer cette méthode comme @Nullable.

5
Martin Vejmelka

Je recommanderais d'utiliser le modèle Null Object autant que possible. Il simplifie le code lors de l'appel de vos méthodes, et vous ne lisez pas le code laid comme

if (someObject != null && someObject.someMethod () != whateverValue)

Par exemple, si une méthode renvoie une collection d'objets qui correspondent à un modèle, le retour d'une collection vide est plus logique que le retour de null, et l'itération sur cette collection vide n'aura pratiquement aucune pénalité de performance. Un autre cas serait une méthode qui renvoie l'instance de classe utilisée pour enregistrer les données, renvoyer un objet Null plutôt que null est préférable à mon avis, car elle n'oblige pas les utilisateurs à toujours vérifier si la référence retournée est null.

Dans les cas où renvoyer null est logique (appeler la méthode findPerson () par exemple), j'essaierais au moins de fournir une méthode qui renvoie si l'objet est présent (personExists (int personId) par exemple), un autre exemple serait la méthode containsKey () sur un Map en Java). Cela rend le code des appelants plus propre, car vous pouvez facilement voir qu'il existe une possibilité que l'objet souhaité ne soit pas disponible (la personne n'existe pas, la clé n'est pas présente sur la carte). Vérifier constamment si une référence est null obscurcit le code à mon avis.

3
Laf

null est la meilleure chose à retourner si et seulement si les conditions suivantes s'appliquent:

  • le résultat nul est attendu en fonctionnement normal. On peut s'attendre à ce que vous ne puissiez pas trouver une personne dans certaines circonstances raisonnables, donc findPerson () renvoyant null est très bien. Cependant, s'il s'agit d'une défaillance véritablement inattendue (par exemple, dans une fonction appelée saveMyImportantData ()), vous devez alors lever une exception. Il y a un indice dans le nom - les exceptions sont pour des circonstances exceptionnelles!
  • null est utilisé pour signifier "non trouvé/aucune valeur". Si vous voulez dire autre chose, renvoyez autre chose! De bons exemples seraient les opérations en virgule flottante renvoyant Infinity ou NaN - ce sont des valeurs avec des significations spécifiques, donc cela deviendrait très déroutant si vous retourniez null ici.
  • La fonction est destinée à renvoyer une seule valeur, comme findPerson (). S'il a été conçu pour renvoyer une collection, par exemple findAllOldPeople () alors une collection vide est la meilleure. Un corollaire de ceci est qu'une fonction qui retourne une collection ne doit jamais retourner null .

De plus, assurez-vous que vous documentez le fait que la fonction peut retourner null.

Si vous suivez ces règles, les valeurs nulles sont pour la plupart inoffensives. Notez que si vous oubliez de vérifier la valeur null, vous obtiendrez normalement une NullPointerException immédiatement après, ce qui est généralement un bug assez facile à corriger. Cette approche rapide est bien meilleure que d'avoir une fausse valeur de retour (par exemple une chaîne vide) qui se propage tranquillement autour de votre système, corrompant éventuellement les données, sans levant une exception.

Enfin, si vous appliquez ces règles aux deux fonctions répertoriées dans la question:

  • FindPerson - il serait approprié que cette fonction retourne null si la personne n'a pas été trouvée
  • ReverseString - semble qu'il ne manquerait jamais de trouver un résultat si passé une chaîne (puisque toutes les chaînes peuvent être inversées, y compris la chaîne vide). Il ne doit donc jamais retourner null. Si quelque chose ne va pas (mémoire insuffisante?), Il devrait alors lever une exception.
3
mikera

À mon avis, il y a une différence entre retourner NULL, retourner un résultat vide (par exemple la chaîne vide ou une liste vide) et lever une exception.

J'adopte normalement l'approche suivante. Je considère un appel de fonction ou méthode f (v1, ..., vn) comme l'application d'une fonction

f : S x T1 x ... x Tn -> T

où S c'est "l'état du monde" T1, ..., Tn sont les types de paramètres d'entrée, et T est le type de retour.

J'essaie d'abord de définir cette fonction. Si la fonction est partielle (c'est-à-dire qu'il existe des valeurs d'entrée pour lesquelles elle n'est pas définie), je renvoie NULL pour signaler cela. C'est parce que je veux que le calcul se termine normalement et me dise que la fonction que j'ai demandée n'est pas définie sur les entrées données. L'utilisation, par exemple, d'une chaîne vide comme valeur de retour est ambiguë car il se pourrait que la fonction soit définie sur les entrées et que la chaîne vide soit le résultat correct.

Je pense que la vérification supplémentaire d'un pointeur NULL dans le code appelant est nécessaire parce que vous appliquez une fonction partielle et c'est la tâche de la méthode appelée de vous dire si la fonction n'est pas définie pour le donné contribution.

Je préfère utiliser des exceptions pour les erreurs qui ne permettent pas d'effectuer le calcul (c'est-à-dire qu'il n'a pas été possible de trouver de réponse).

Par exemple, supposons que j'ai une classe Customer et que je souhaite implémenter une méthode

Customer findCustomer(String customerCode)

pour rechercher un client dans la base de données d'application par son code. Dans cette méthode, je

  • Renvoyer un objet de classe Customer si la requête aboutit,
  • Renvoyez null si la requête ne trouve aucun client.
  • Lancez une exception s'il n'est pas possible de se connecter à la base de données.

Les contrôles supplémentaires pour null, par exemple.

Customer customer = findCustomer("...");
if (customer != null && customer.getOrders() > 0)
{
    ...
}

font partie de la sémantique de ce que je fais et je ne voudrais pas simplement "les sauter" afin d'améliorer la lecture du code. Je ne pense pas que ce soit une bonne pratique de simplifier la sémantique du problème en question juste pour simplifier le code.

Bien sûr, puisque la vérification de null se produit très souvent, est-ce bien si le langage prend en charge une syntaxe spéciale pour cela.

J'envisagerais également d'utiliser le modèle Null Object (comme suggéré par Laf) tant que je peux distinguer l'objet nul d'une classe de tous les autres objets.

1
Giorgio

Ajout à ce que les gens ont déjà dit sur le constructeur de type Maybe:

Un autre gros gain, en plus de ne pas risquer de NPE, est le sémantique. Dans le monde fonctionnel, où nous traitons de fonctions pures, nous aimons essayer de créer des fonctions qui, lorsqu'elles sont appliquées, sont exactement équivalentes à leur valeur de retour. Considérons le cas de String.reverse(): il s'agit d'une fonction qui, étant donné une certaine chaîne, représente la version inversée de cette chaîne. Nous savons que cette chaîne existe, car chaque chaîne peut être inversée (les chaînes sont essentiellement des ensembles de caractères ordonnés).

Maintenant, qu'en est-il de findCustomer(int id)? Cette fonction représente le client qui a l'ID donné. C'est quelque chose qui peut ou non exister. Mais rappelez-vous, les fonctions ont une seule valeur de retour, vous ne pouvez donc pas vraiment dire "cette fonction renvoie un client avec l'ID donné OU elle renvoie null.

C'est mon principal problème à la fois avec le retour de null et le retour d'un objet nul. Ils sont trompeurs. null n'est pas un client, et ce n'est pas vraiment "l'absence de client" non plus. C'est l'absence de TOUT. C'est juste un pointeur sans type vers RIEN. Très bas niveau et très moche et très sujet aux erreurs. Un objet nul est également assez trompeur ici, je pense. Un client nul IS un client. Ce n'est pas l'absence d'un, ce n'est pas le client avec l'ID demandé, donc le renvoyer est simplement FAUX. Ce n'est PAS le client que j'ai demandé, mais vous prétendez toujours avoir renvoyé un client. Il a le mauvais ID, c'est clairement un bogue. Il rompt le contrat suggéré par le nom de la méthode et la signature. Il faut que le code client assume des choses sur votre conception ou lise la documentation.

Ces deux problèmes sont résolus par un Maybe Customer. Du coup, on ne dit pas "cette fonction représente le client avec l'ID donné". Nous disons "cette fonction représente le client avec l'ID donné si elle existe. Elle est maintenant très claire et explicite sur ce qu'elle fait. Si vous demandez le client avec un ID inexistant, vous recevra Nothing. Comment est-ce mieux que null? Pas seulement pour le risque NPE. Mais aussi parce que le type de Nothing n'est pas seulement Maybe C'est Maybe Customer. Il a une signification sémantique. Cela signifie spécifiquement "l'absence d'un client", indiquant qu'un tel client n'existe pas.

Un autre problème avec null est bien sûr qu'il est ambigu. Est-ce que vous n'avez pas trouvé le client, ou y a-t-il eu une erreur de connexion à la base de données ou ne suis-je simplement pas autorisé à voir ce client?

Si vous devez gérer plusieurs cas d'erreur comme celui-ci, vous pouvez soit lever des exceptions pour ces versions, soit (et je préfère cette méthode) renvoyer un Either CustomerLoadError Customer, représentant soit un problème ou le client retourné. Il a tous les avantages de Maybe Customer tout en vous permettant également de spécifier ce qui peut mal tourner.

La principale chose que je recherche est de capturer le contrat de la fonction dans sa signature. Ne présumez rien, ne vous fiez pas à des conventions éphémères, soyez explicite.

1
sara

1) Si la sémantique de la fonction est qu'elle ne peut rien retourner, l'appelant DOIT la tester, sinon il le fera par exemple. prenez votre argent et ne le donnez à personne.

2) Il est bon de créer des fonctions qui retournent toujours sémantiquement quelque chose (ou lancer).

Avec un logger, il est logique de renvoyer un logger qui ne se connecte pas si aucun logger n'a été défini. Lors du retour d'une collection, cela n'a presque jamais de sens (sauf au "niveau suffisamment bas", où la collection elle-même est LA donnée, pas ce qu'elle contient) pour ne rien retourner, car un ensemble vide est un ensemble, pas rien.

Avec un exemple personne, j'irais en "mise en veille prolongée" (get vs load, IIRC, mais là c'est compliqué par des objets proxy pour le chargement paresseux) d'avoir deux fonctions, une qui retourne null et la seconde qui jette.

0
user470365

Les méthodes renvoyant des collections doivent retourner vides, mais d'autres pourraient retourner null, car pour les collections, il se peut que vous ayez un objet mais qu'il n'y ait aucun élément, donc l'appelant validera juste la taille plutôt les deux.

0
Phani

Pour moi, il y a deux cas ici. Si vous retournez une sorte de liste, vous devez toujours retourner la liste, quoi qu'il en soit, vide si vous n'avez rien à y mettre.

Le seul cas où je vois un débat est lorsque vous retournez un seul article. Je me retrouve préférant retourner nul en cas d'échec dans une telle situation sur la base de faire échouer les choses rapidement.

Si vous renvoyez un objet nul et que l'appelant avait besoin d'un objet réel, ils pourraient continuer et essayer d'utiliser l'objet nul produisant un comportement inattendu. Je pense qu'il vaut mieux que la routine aille bien s'ils oublient de faire face à la situation. Si quelque chose ne va pas, je veux une exception dès que possible. Vous êtes moins susceptible d'expédier un bug de cette façon.

0
Loren Pechtel