C'est une sorte de question générale (mais j'utilise C #), quelle est la meilleure façon (meilleure pratique), renvoyez-vous une collection nulle ou vide pour une méthode qui a une collection comme type de retour?
C'est une sorte de question générale (mais j'utilise C #), quelle est la meilleure façon (meilleure pratique), renvoyez-vous une collection nulle ou vide pour une méthode qui a une collection comme type de retour?
Réponses:
Collection vide. Toujours.
Cela craint:
if(myInstance.CollectionProperty != null)
{
foreach(var item in myInstance.CollectionProperty)
/* arrgh */
}
Il est considéré comme une meilleure pratique de ne JAMAIS retourner null
lors du retour d'une collection ou d'un énumérable. TOUJOURS retourner une collection / énumérable vide. Il empêche les absurdités susmentionnées et empêche votre voiture d'être incitée par les collègues et les utilisateurs de vos classes.
Lorsque vous parlez de propriétés, définissez toujours votre propriété une fois et oubliez-la
public List<Foo> Foos {public get; private set;}
public Bar() { Foos = new List<Foo>(); }
Dans .NET 4.6.1, vous pouvez condenser cela beaucoup:
public List<Foo> Foos { get; } = new List<Foo>();
Lorsque vous parlez de méthodes qui renvoient des énumérateurs, vous pouvez facilement renvoyer un énumérateur vide au lieu de null
...
public IEnumerable<Foo> GetMyFoos()
{
return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}
L'utilisation Enumerable.Empty<T>()
peut être considérée comme plus efficace que le retour, par exemple, d'une nouvelle collection ou d'un tableau vide.
IEnumerable
ou ICollection
non. Quoi qu'il en soit, si vous sélectionnez quelque chose de type, ICollection
ils reviennent également null
... J'aimerais qu'ils retournent une collection vide, mais je les ai rencontrés en retour null
, alors j'ai pensé que je le mentionnerais ici. Je dirais que la valeur par défaut d'une collection d'énumérables est vide et non nulle. Je ne savais pas que c'était un sujet aussi sensible.
Extrait des Framework Design Guidelines 2nd Edition (p. 256):
NE retournez PAS de valeurs nulles à partir des propriétés de la collection ou des méthodes renvoyant des collections. Renvoyez une collection vide ou un tableau vide à la place.
Voici un autre article intéressant sur les avantages de ne pas retourner de null (j'essayais de trouver quelque chose sur le blog de Brad Abram, et il avait un lien vers l'article).
Edit- comme Eric Lippert a maintenant commenté la question d'origine, je voudrais également faire un lien vers son excellent article .
Cela dépend de votre contrat et de votre cas concret . En règle générale, il est préférable de renvoyer les collections vides , mais parfois ( rarement ):
null
pourrait signifier quelque chose de plus spécifique;null
.Quelques exemples concrets:
null
cela signifierait que l'élément est manquant, tandis qu'une collection vide rendrait un élément redondant (et peut-être incorrect)<collection />
Il y a un autre point qui n'a pas encore été mentionné. Considérez le code suivant:
public static IEnumerable<string> GetFavoriteEmoSongs()
{
yield break;
}
Le langage C # renverra un énumérateur vide lors de l'appel de cette méthode. Par conséquent, pour être cohérent avec la conception du langage (et donc les attentes du programmeur), une collection vide doit être retournée.
Le vide est beaucoup plus convivial pour le consommateur.
Il existe une méthode claire pour créer un énumérable vide:
Enumerable.Empty<Element>()
Il me semble que vous devez renvoyer la valeur sémantiquement correcte dans son contexte, quelle qu'elle soit. Une règle qui dit "toujours retourner une collection vide" me semble un peu simpliste.
Supposons, par exemple, dans un système pour un hôpital, que nous ayons une fonction qui est censée renvoyer une liste de toutes les hospitalisations précédentes au cours des 5 dernières années. Si le client n'est pas allé à l'hôpital, il est judicieux de renvoyer une liste vide. Mais que se passe-t-il si le client laisse cette partie du formulaire d'admission vierge? Nous avons besoin d'une valeur différente pour distinguer "liste vide" de "pas de réponse" ou "ne sait pas". Nous pourrions lever une exception, mais ce n'est pas nécessairement une condition d'erreur, et cela ne nous exclut pas nécessairement du flux de programme normal.
J'ai souvent été frustré par des systèmes qui ne peuvent pas faire la distinction entre zéro et aucune réponse. J'ai eu un certain nombre de fois où un système m'a demandé d'entrer un certain nombre, j'entre zéro et je reçois un message d'erreur me disant que je dois entrer une valeur dans ce champ. Je viens de le faire: je suis entré à zéro! Mais il n'acceptera pas zéro car il ne peut le distinguer d'aucune réponse.
Répondre à Saunders:
Oui, je suppose qu'il y a une différence entre «La personne n'a pas répondu à la question» et «La réponse était zéro». C'était le point du dernier paragraphe de ma réponse. De nombreux programmes sont incapables de distinguer «ne sait pas» de vide ou zéro, ce qui me semble être une faille potentiellement grave. Par exemple, je cherchais une maison il y a environ un an. Je suis allé sur un site Web immobilier et il y avait beaucoup de maisons répertoriées avec un prix demandé de 0 $. Cela me semblait plutôt bien: ils donnent ces maisons gratuitement! Mais je suis sûr que la triste réalité était qu'ils n'avaient tout simplement pas entré le prix. Dans ce cas, vous pouvez dire: «Eh bien, bien sûr, zéro signifie qu'ils n'ont pas entré le prix - personne ne va céder une maison gratuitement». Mais le site a également répertorié les prix moyens de vente et de demande des maisons dans diverses villes. Je ne peux pas m'empêcher de me demander si la moyenne n'a pas inclus les zéros, donnant ainsi une moyenne incorrectement faible pour certains endroits. c'est-à-dire quelle est la moyenne de 100 000 $; 120 000 $; et "je ne sais pas"? Techniquement, la réponse est "ne sais pas". Ce que nous voulons probablement vraiment, c'est 110 000 $. Mais ce que nous obtiendrons probablement, c'est 73 333 $, ce qui serait complètement faux. Et si nous avions ce problème sur un site où les utilisateurs peuvent commander en ligne? (Peu probable pour l'immobilier, mais je suis sûr que vous l'avez vu pour de nombreux autres produits.) Voudrions-nous vraiment que "prix non spécifié pour le moment" soit interprété comme "gratuit"? donnant ainsi une moyenne incorrectement faible pour certains endroits. c'est-à-dire quelle est la moyenne de 100 000 $; 120 000 $; et "je ne sais pas"? Techniquement, la réponse est "ne sais pas". Ce que nous voulons probablement vraiment, c'est 110 000 $. Mais ce que nous obtiendrons probablement, c'est 73 333 $, ce qui serait complètement faux. Et si nous avions ce problème sur un site où les utilisateurs peuvent commander en ligne? (Peu probable pour l'immobilier, mais je suis sûr que vous l'avez vu pour de nombreux autres produits.) Voudrions-nous vraiment que "prix non spécifié pour le moment" soit interprété comme "gratuit"? donnant ainsi une moyenne incorrectement faible pour certains endroits. c'est-à-dire quelle est la moyenne de 100 000 $; 120 000 $; et "je ne sais pas"? Techniquement, la réponse est "ne sais pas". Ce que nous voulons probablement vraiment, c'est 110 000 $. Mais ce que nous obtiendrons probablement, c'est 73 333 $, ce qui serait complètement faux. Et si nous avions ce problème sur un site où les utilisateurs peuvent commander en ligne? (Peu probable pour l'immobilier, mais je suis sûr que vous l'avez vu pour de nombreux autres produits.) Voudrions-nous vraiment que "prix non spécifié pour le moment" soit interprété comme "gratuit"? ce qui serait complètement faux. Et si nous avions ce problème sur un site où les utilisateurs peuvent commander en ligne? (Peu probable pour l'immobilier, mais je suis sûr que vous l'avez vu pour de nombreux autres produits.) Voudrions-nous vraiment que "prix non spécifié pour le moment" soit interprété comme "gratuit"? ce qui serait complètement faux. Et si nous avions ce problème sur un site où les utilisateurs peuvent commander en ligne? (Peu probable pour l'immobilier, mais je suis sûr que vous l'avez vu pour de nombreux autres produits.) Voudrions-nous vraiment que "prix non spécifié pour le moment" soit interprété comme "gratuit"?
RE ayant deux fonctions distinctes, un "y en a-t-il?" et un "si oui, qu'est-ce que c'est?" Oui, vous pourriez certainement le faire, mais pourquoi voudriez-vous? Maintenant, le programme appelant doit passer deux appels au lieu d'un. Que se passe-t-il si un programmeur ne parvient pas à appeler le "tout?" et va directement au "qu'est-ce que c'est?" ? Le programme renverra-t-il un zéro erroné? Jetez une exception? Renvoyer une valeur indéfinie? Il crée plus de code, plus de travail et plus d'erreurs potentielles.
Le seul avantage que je vois est qu'il vous permet de vous conformer à une règle arbitraire. Y a-t-il un avantage à cette règle qui vaut la peine de lui obéir? Sinon, pourquoi s'embêter?
Répondre à Jammycakes:
Considérez à quoi ressemblerait le code réel. Je sais que la question a dit C # mais excusez-moi si j'écris Java. Mon C # n'est pas très net et le principe est le même.
Avec un retour nul:
HospList list=patient.getHospitalizationList(patientId);
if (list==null)
{
// ... handle missing list ...
}
else
{
for (HospEntry entry : list)
// ... do whatever ...
}
Avec une fonction distincte:
if (patient.hasHospitalizationList(patientId))
{
// ... handle missing list ...
}
else
{
HospList=patient.getHospitalizationList(patientId))
for (HospEntry entry : list)
// ... do whatever ...
}
C'est en fait une ligne ou deux de moins avec le retour nul, donc ce n'est pas plus de fardeau pour l'appelant, c'est moins.
Je ne vois pas comment cela crée un problème SEC. Ce n'est pas comme si nous devions exécuter l'appel deux fois. Si nous avons toujours voulu faire la même chose lorsque la liste n'existe pas, nous pourrions peut-être pousser la gestion vers la fonction get-list plutôt que de demander à l'appelant de le faire, et donc mettre le code dans l'appelant serait une violation DRY. Mais nous ne voulons presque certainement pas toujours faire la même chose. Dans les fonctions où nous devons avoir la liste à traiter, une liste manquante est une erreur qui pourrait bien arrêter le traitement. Mais sur un écran d'édition, nous ne voulons certainement pas arrêter le traitement s'ils n'ont pas encore entré de données: nous voulons les laisser entrer des données. Ainsi, la gestion de "pas de liste" doit se faire au niveau de l'appelant d'une manière ou d'une autre. Et que nous le fassions avec un retour nul ou une fonction séparée ne fait aucune différence avec le principe plus large.
Bien sûr, si l'appelant ne vérifie pas null, le programme peut échouer avec une exception de pointeur nul. Mais s'il y a une fonction séparée "got any" et que l'appelant n'appelle pas cette fonction mais appelle aveuglément la fonction "get list", alors que se passe-t-il? S'il lance une exception ou échoue, eh bien, c'est à peu près la même chose que ce qui se passerait s'il retournait null et ne le vérifiait pas. S'il renvoie une liste vide, c'est tout simplement faux. Vous ne parvenez pas à faire la distinction entre "J'ai une liste avec zéro élément" et "Je n'ai pas de liste". C'est comme retourner zéro pour le prix lorsque l'utilisateur n'a entré aucun prix: c'est juste faux.
Je ne vois pas en quoi l'attachement d'un attribut supplémentaire à la collection aide. L'appelant doit encore le vérifier. Comment est-ce mieux que de vérifier null? Encore une fois, la pire chose qui puisse arriver est que le programmeur oublie de le vérifier et donne des résultats incorrects.
Une fonction qui renvoie null n'est pas une surprise si le programmeur est familier avec le concept de null signifiant "ne pas avoir de valeur", ce que je pense que tout programmeur compétent aurait dû entendre, qu'il pense que c'est une bonne idée ou non. Je pense qu'avoir une fonction séparée est plus un problème de "surprise". Si un programmeur n'est pas familier avec l'API, lorsqu'il exécute un test sans données, il découvre rapidement que parfois il récupère une valeur nulle. Mais comment pourrait-il découvrir l'existence d'une autre fonction à moins qu'il ne lui vienne à l'esprit qu'il pourrait y avoir une telle fonction et qu'il vérifie la documentation, et que la documentation soit complète et compréhensible? Je préfère de loin avoir une fonction qui me donne toujours une réponse significative, plutôt que deux fonctions que je dois connaître et me rappeler d'appeler les deux.
Si une collection vide a un sens sémantique, c'est ce que je préfère retourner. Renvoyer une collection vide pour les GetMessagesInMyInbox()
communications "vous n'avez vraiment aucun message dans votre boîte de réception", alors que le retour null
peut être utile pour indiquer que les données sont insuffisantes pour dire à quoi devrait ressembler la liste qui pourrait être retournée.
null
valeur ne semble certainement pas raisonnable, je pensais en termes plus généraux à celle-ci. Les exceptions sont également importantes pour communiquer le fait que quelque chose a mal tourné, mais si les "données insuffisantes" auxquelles il est fait référence sont parfaitement attendues, le fait de lever une exception entraînerait une mauvaise conception. Je pense plutôt à un scénario où c'est parfaitement possible et sans erreur du tout pour que la méthode ne puisse parfois pas calculer une réponse.
Renvoyer null pourrait être plus efficace, car aucun nouvel objet n'est créé. Cependant, cela nécessiterait également souvent une null
vérification (ou une gestion des exceptions).
Sémantiquement, null
et une liste vide ne signifie pas la même chose. Les différences sont subtiles et un choix peut être meilleur que l'autre dans des cas spécifiques.
Quel que soit votre choix, documentez-le pour éviter toute confusion.
On pourrait faire valoir que le raisonnement derrière Null Object Pattern est similaire à celui en faveur du retour de la collection vide.
Dépend de la situation. S'il s'agit d'un cas spécial, retournez null. Si la fonction arrive juste à renvoyer une collection vide, alors évidemment, c'est ok. Cependant, renvoyer une collection vide en tant que cas spécial en raison de paramètres non valides ou d'autres raisons n'est PAS une bonne idée, car il masque une condition de cas spécial.
En fait, dans ce cas, je préfère généralement lever une exception pour m'assurer qu'elle n'est VRAIMENT pas ignorée :)
Dire que cela rend le code plus robuste (en renvoyant une collection vide) car ils n'ont pas à gérer la condition nulle est mauvais, car il masque simplement un problème qui devrait être géré par le code appelant.
Je dirais que ce null
n'est pas la même chose qu'une collection vide et vous devez choisir celle qui représente le mieux ce que vous retournez. Dans la plupart des cas, il null
n'y a rien (sauf en SQL). Une collection vide est quelque chose, quoique quelque chose de vide.
Si vous devez choisir l'un ou l'autre, je dirais que vous devriez tendre vers une collection vide plutôt que null. Mais il y a des moments où une collection vide n'est pas la même chose qu'une valeur nulle.
Pensez toujours en faveur de vos clients (qui utilisent votre API):
Le retour de 'null' pose très souvent des problèmes avec les clients qui ne gèrent pas correctement les vérifications nulles, ce qui provoque une NullPointerException pendant l'exécution. J'ai vu des cas où une telle vérification nulle manquante a forcé un problème de production prioritaire (un client utilisé pour chaque (...) sur une valeur nulle). Pendant le test, le problème ne s'est pas produit, car les données opérées étaient légèrement différentes.
J'aime donner des explications ici, avec un exemple approprié.
Considérons un cas ici ..
int totalValue = MySession.ListCustomerAccounts()
.FindAll(ac => ac.AccountHead.AccountHeadID
== accountHead.AccountHeadID)
.Sum(account => account.AccountValue);
Considérons ici les fonctions que j'utilise ..
1. ListCustomerAccounts() // User Defined
2. FindAll() // Pre-defined Library Function
Je peux facilement utiliser ListCustomerAccount
et FindAll
au lieu de.,
int totalValue = 0;
List<CustomerAccounts> custAccounts = ListCustomerAccounts();
if(custAccounts !=null ){
List<CustomerAccounts> custAccountsFiltered =
custAccounts.FindAll(ac => ac.AccountHead.AccountHeadID
== accountHead.AccountHeadID );
if(custAccountsFiltered != null)
totalValue = custAccountsFiltered.Sum(account =>
account.AccountValue).ToString();
}
REMARQUE: Étant donné que AccountValue ne l'est pas null
, la fonction Sum () ne retournera pas null
. Par conséquent, je peux l'utiliser directement.
Le retour d'une collection vide est préférable dans la plupart des cas.
La raison en est la commodité de la mise en œuvre de l'appelant, un contrat cohérent et une mise en œuvre plus facile.
Si une méthode renvoie null pour indiquer un résultat vide, l'appelant doit implémenter un adaptateur de vérification null en plus de l'énumération. Ce code est ensuite dupliqué dans différents appelants, alors pourquoi ne pas mettre cet adaptateur dans la méthode afin qu'il puisse être réutilisé.
Une utilisation valide de null pour IEnumerable peut être une indication de résultat absent ou d'échec d'une opération, mais dans ce cas, d'autres techniques doivent être envisagées, telles que la levée d'une exception.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
namespace StackOverflow.EmptyCollectionUsageTests.Tests
{
/// <summary>
/// Demonstrates different approaches for empty collection results.
/// </summary>
class Container
{
/// <summary>
/// Elements list.
/// Not initialized to an empty collection here for the purpose of demonstration of usage along with <see cref="Populate"/> method.
/// </summary>
private List<Element> elements;
/// <summary>
/// Gets elements if any
/// </summary>
/// <returns>Returns elements or empty collection.</returns>
public IEnumerable<Element> GetElements()
{
return elements ?? Enumerable.Empty<Element>();
}
/// <summary>
/// Initializes the container with some results, if any.
/// </summary>
public void Populate()
{
elements = new List<Element>();
}
/// <summary>
/// Gets elements. Throws <see cref="InvalidOperationException"/> if not populated.
/// </summary>
/// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>.</returns>
public IEnumerable<Element> GetElementsStrict()
{
if (elements == null)
{
throw new InvalidOperationException("You must call Populate before calling this method.");
}
return elements;
}
/// <summary>
/// Gets elements, empty collection or nothing.
/// </summary>
/// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with zero or more elements, or null in some cases.</returns>
public IEnumerable<Element> GetElementsInconvenientCareless()
{
return elements;
}
/// <summary>
/// Gets elements or nothing.
/// </summary>
/// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with elements, or null in case of empty collection.</returns>
/// <remarks>We are lucky that elements is a List, otherwise enumeration would be needed.</remarks>
public IEnumerable<Element> GetElementsInconvenientCarefull()
{
if (elements == null || elements.Count == 0)
{
return null;
}
return elements;
}
}
class Element
{
}
/// <summary>
/// http://stackoverflow.com/questions/1969993/is-it-better-to-return-null-or-empty-collection/
/// </summary>
class EmptyCollectionTests
{
private Container container;
[SetUp]
public void SetUp()
{
container = new Container();
}
/// <summary>
/// Forgiving contract - caller does not have to implement null check in addition to enumeration.
/// </summary>
[Test]
public void UseGetElements()
{
Assert.AreEqual(0, container.GetElements().Count());
}
/// <summary>
/// Forget to <see cref="Container.Populate"/> and use strict method.
/// </summary>
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void WrongUseOfStrictContract()
{
container.GetElementsStrict().Count();
}
/// <summary>
/// Call <see cref="Container.Populate"/> and use strict method.
/// </summary>
[Test]
public void CorrectUsaOfStrictContract()
{
container.Populate();
Assert.AreEqual(0, container.GetElementsStrict().Count());
}
/// <summary>
/// Inconvenient contract - needs a local variable.
/// </summary>
[Test]
public void CarefulUseOfCarelessMethod()
{
var elements = container.GetElementsInconvenientCareless();
Assert.AreEqual(0, elements == null ? 0 : elements.Count());
}
/// <summary>
/// Inconvenient contract - duplicate call in order to use in context of an single expression.
/// </summary>
[Test]
public void LameCarefulUseOfCarelessMethod()
{
Assert.AreEqual(0, container.GetElementsInconvenientCareless() == null ? 0 : container.GetElementsInconvenientCareless().Count());
}
[Test]
public void LuckyCarelessUseOfCarelessMethod()
{
// INIT
var praySomeoneCalledPopulateBefore = (Action)(()=>container.Populate());
praySomeoneCalledPopulateBefore();
// ACT //ASSERT
Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
}
/// <summary>
/// Excercise <see cref="ArgumentNullException"/> because of null passed to <see cref="Enumerable.Count{TSource}(System.Collections.Generic.IEnumerable{TSource})"/>
/// </summary>
[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void UnfortunateCarelessUseOfCarelessMethod()
{
Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
}
/// <summary>
/// Demonstrates the client code flow relying on returning null for empty collection.
/// Exception is due to <see cref="Enumerable.First{TSource}(System.Collections.Generic.IEnumerable{TSource})"/> on an empty collection.
/// </summary>
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void UnfortunateEducatedUseOfCarelessMethod()
{
container.Populate();
var elements = container.GetElementsInconvenientCareless();
if (elements == null)
{
Assert.Inconclusive();
}
Assert.IsNotNull(elements.First());
}
/// <summary>
/// Demonstrates the client code is bloated a bit, to compensate for implementation 'cleverness'.
/// We can throw away the nullness result, because we don't know if the operation succeeded or not anyway.
/// We are unfortunate to create a new instance of an empty collection.
/// We might have already had one inside the implementation,
/// but it have been discarded then in an effort to return null for empty collection.
/// </summary>
[Test]
public void EducatedUseOfCarefullMethod()
{
Assert.AreEqual(0, (container.GetElementsInconvenientCarefull() ?? Enumerable.Empty<Element>()).Count());
}
}
}
Je l'appelle mon erreur d'un milliard de dollars… À cette époque, je concevais le premier système de type complet pour les références dans un langage orienté objet. 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. - Tony Hoare, inventeur d'ALGOL W.
Voir ici pour une tempête de merde élaborée sur null
en général. Je ne suis pas d'accord avec la déclaration qui en undefined
est une autre null
, mais elle vaut quand même la peine d'être lue. Et cela explique pourquoi vous devriez éviter null
du tout et pas seulement dans le cas que vous avez demandé. L'essence est, c'est null
dans n'importe quelle langue un cas spécial. Vous devez penser à null
une exception. undefined
est différent dans ce sens, ce code traitant d'un comportement indéfini n'est dans la plupart des cas qu'un bogue. C et la plupart des autres langues ont également un comportement non défini, mais la plupart d'entre elles n'ont pas d'identifiant pour cela dans la langue.
Du point de vue de la gestion de la complexité, objectif principal de l'ingénierie logicielle, nous voulons éviter de propager une complexité cyclomatique inutile aux clients d'une API. Renvoyer un null au client, c'est comme lui renvoyer le coût de complexité cyclomatique d'une autre branche de code.
(Cela correspond à une charge de test unitaire. Vous devez écrire un test pour le cas de retour nul, en plus du cas de retour de la collection vide.)