web-dev-qa-db-fra.com

Est-il préférable de renvoyer une collection nulle ou vide?

C'est une sorte de question générale (mais j'utilise C #), quelle est la meilleure méthode (meilleure pratique), renvoyez-vous une collection nulle ou vide pour une méthode ayant une collection comme type de retour?

386
Omu

Collection vide. Toujours.

Cela craint:

if(myInstance.CollectionProperty != null)
{
  foreach(var item in myInstance.CollectionProperty)
    /* arrgh */
}

Il est recommandé de ne JAMAIS retourner null lors du retour d'une collection ou d'une liste énumérable. TOUJOURS retourne une énumération/collection vide. Cela évite les absurdités susmentionnées et empêche votre voiture de se faire piquer par des collègues et des utilisateurs de vos cours.

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 ceci assez souvent:

public List<Foo> Foos { get; } = new List<Foo>();

Lorsque vous parlez de méthodes qui renvoient enumerables, vous pouvez facilement renvoyer un énumérable vide au lieu de null...

public IEnumerable<Foo> GetMyFoos()
{
  return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}

L'utilisation de Enumerable.Empty<T>() peut être considérée comme plus efficace que de renvoyer, par exemple, une nouvelle collection ou un nouveau tableau vide.

464
Will

De la Framework Design Guidelines 2nd Edition (p. 256):

NE retournez PAS de valeurs NULL à partir de propriétés de collection ou de méthodes renvoyant des collections. Renvoie une collection vide ou un tableau vide à la place.

Voici un autre article intéressant sur les avantages de ne pas renvoyer les valeurs NULL (j'essayais de trouver quelque chose sur le blog de Brad Abram, et il a lié à l'article).

Modifiez - comme Eric Lippert a commenté la question d'origine, j'aimerais aussi lien vers son excellent article .

149
RichardOD

Dépend de votre contrat et de votre cas concret . Généralement il est préférable de retourner des collections vides, mais parfois ( rarement ):

  • null pourrait vouloir dire quelque chose de plus spécifique;
  • votre API (contrat) peut vous obliger à retourner null.

Quelques exemples concrets:

  • un composant d'interface utilisateur (à partir d'une bibliothèque hors de votre contrôle) peut rendre une table vide si une collection vide est transmise, ou aucune table du tout, si la valeur null est transmise.
  • dans un objet à XML (JSON/peu importe), où null signifierait que l'élément est manquant, alors qu'une collection vide rendrait un redondant (et éventuellement une erreur) <collection />
  • vous utilisez ou implémentez une API qui stipule explicitement que null doit être renvoyé/passé
88
Bozho

Il y a un autre point qui n'a pas encore été mentionné. Considérons 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 correspondre à la conception du langage (et donc aux attentes du programmeur), une collection vide doit être renvoyée.

36
Jeffrey L Whitledge

Vide est beaucoup plus convivial.

Il existe une méthode claire pour créer un énumérable vide:

Enumerable.Empty<Element>()
30
George Polevoy

Il me semble que vous devriez retourner la valeur qui est sémantiquement correcte dans son contexte, quel qu’il soit. Une règle qui dit "retourne toujours une collection vide" me semble un peu simpliste.

Supposons, par exemple, dans un système pour un hôpital, nous avons une fonction qui est supposée renvoyer une liste de toutes les hospitalisations antérieures des 5 dernières années. Si le client n'a pas été à 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 en blanc? Nous avons besoin d’une valeur différente pour distinguer "liste vide" de "pas de réponse" ou "ne sait pas". Nous pourrions lancer une exception, mais ce n'est pas nécessairement une condition d'erreur et cela ne nous oblige pas nécessairement à sortir du flux de programme normal.

J'ai souvent été frustré par les systèmes qui ne peuvent pas faire la distinction entre zéro et aucune réponse. Un certain nombre de fois où un système m'a demandé de saisir un nombre, j'entre zéro et un message d'erreur m'indique 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 pas le distinguer de l'absence de 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 but du dernier paragraphe de ma réponse. De nombreux programmes sont incapables de distinguer "ne sait pas" de rien ou de zéro, ce qui me semble 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 énumérées avec un prix demandé de 0 $. Cela me semblait très bien: ils donnent ces maisons gratuitement! Mais je suis sûr que la triste réalité était qu’ils n’étaient tout simplement pas entrés dans le prix. Dans ce cas, vous pouvez dire: "Bien évidemment, zéro signifie qu'ils n'ont pas entré le prix - personne ne va céder une maison gratuitement." Mais le site indiquait également les prix moyens de vente et de vente des maisons dans différentes villes. Je ne peux pas m'empêcher de me demander si la moyenne n'inclut pas les zéros, ce qui donne une moyenne erronément basse pour certains endroits. c'est-à-dire quelle est la moyenne de 100 000 dollars; 120 000 dollars; et "je ne sais pas"? Techniquement, la réponse est "je ne sais pas". Ce que nous voulons probablement voir, c'est 110 000 $. Mais nous obtiendrons probablement 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 déjà fait pour de nombreux autres produits.) Voudrions-nous vraiment que "prix non précisé" soit interprété comme "gratuit"?

RE ayant deux fonctions distinctes, un "y 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 faire deux appels au lieu d'un. Que se passe-t-il si un programmeur ne parvient pas à appeler "any?" et va directement au "qu'est-ce que c'est?" ? Le programme renverra-t-il un zéro erroné? Lancer une exception? Renvoyer une valeur indéfinie? Cela crée plus de code, plus de travail et plus d'erreurs potentielles.

Le seul avantage que je vois est que cela vous permet de vous conformer à une règle arbitraire. Y a-t-il un avantage à cette règle qui fait qu'il vaut la peine d'y obéir? Si non, pourquoi s'embêter?


Répondre à Jammycakes:

Considérez ce que le code réel ressemblerait. Je connais la question C # mais excusez-moi si j'écris en 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 séparée:

if (patient.hasHospitalizationList(patientId))
{
   // ... handle missing list ...
}
else
{
  HospList=patient.getHospitalizationList(patientId))
  for (HospEntry entry : list)
   // ... do whatever ...
}

Il s’agit en fait d’une ligne ou de deux codes en moins avec le retour nul, de sorte que le fardeau de l’appelant n’est pas alourdi.

Je ne vois pas comment cela crée un problème de DRY. Ce n'est pas comme si nous devions exécuter l'appel deux fois. Si nous voulions toujours faire la même chose quand la liste n’existait pas, nous pourrions peut-être envoyer le traitement Push à la fonction get-list plutôt que de laisser l’appelant le faire, et placer ainsi le code dans l’appelant serait un DRY violation. Mais nous ne voulons sûrement pas toujours faire la même chose. Dans les fonctions pour lesquelles nous devons avoir la liste à traiter, une liste manquante est une erreur qui risque d’arrêter le traitement. Mais sur un écran d'édition, nous ne voulons sûrement pas arrêter le traitement s'ils n'ont pas encore saisi de données: nous voulons leur permettre de saisir des données. Donc, gérer "aucune liste" doit être fait au niveau de l'appelant d'une manière ou d'une autre. Et que nous fassions cela avec un retour nul ou une fonction séparée ne fait aucune différence avec le principe plus général.

Bien sûr, si l'appelant ne vérifie pas la valeur null, le programme peut échouer avec une exception null-pointeur. Mais s'il existe une fonction distincte "got any" et que l'appelant n'appelle pas cette fonction mais appelle aveuglément la fonction "get list", que se passe-t-il? S'il lève une exception ou échoue d'une manière ou d'une autre, eh bien, c'est à peu près la même chose que s'il se produisait s'il renvoyait null et ne le vérifiait pas. Si cela retourne une liste vide, c'est 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 si le prix était égal à zéro lorsque l'utilisateur n'entre aucun prix: c'est faux.

Je ne vois pas en quoi l'attachement d'un attribut supplémentaire à la collection est utile. L'appelant doit encore vérifier. En quoi est-ce mieux que de chercher null? Encore une fois, le pire 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 familiarisé avec le concept de signification nulle "n'a pas de valeur", ce que tout programmeur compétent aurait dû entendre, qu'il pense ou non, est une bonne idée. Je pense qu'avoir une fonction séparée est plus un problème de "surprise". Si un programmeur ne connaît pas bien l’API, lorsqu’il exécute un test sans données, il découvre rapidement qu’il récupère parfois une valeur null. Mais comment pourrait-il découvrir l’existence d’une autre fonction s’il n’avait pas l’impression que cela pourrait exister et qu’il vérifie la documentation et que celle-ci soit complète et compréhensible? Je préférerais de beaucoup avoir une fonction qui me donne toujours une réponse significative, plutôt que deux fonctions que je dois connaître et dont je me souvienne d'appeler les deux.

18
Jay

Si une collection vide a un sens sur le plan sémantique, c'est ce que je préfère retourner. Renvoyer une collection vide pour GetMessagesInMyInbox() communique "vous n'avez vraiment aucun message dans votre boîte de réception", alors que renvoyer null peut être utile pour signaler que des données insuffisantes sont disponibles pour indiquer le type de liste renvoyée. devrait ressembler.

10
David Hedlund

On pourrait soutenir que le raisonnement derrière Null Object Pattern est similaire à celui en faveur du retour de la collection vide.

6
Dan

Retourner null pourrait être plus efficace, car aucun nouvel objet n'est créé. Cependant, un contrôle null (ou une gestion des exceptions) est également souvent nécessaire.

Sémantiquement, null et une liste vide ne signifient pas la même chose. Les différences sont subtiles et un choix peut être meilleur que l'autre dans des cas spécifiques.

Indépendamment de votre choix, documentez-le pour éviter toute confusion.

6
Karmic Coder

Cela dépend de la situation. S'il s'agit d'un cas particulier, renvoyez null. Si la fonction renvoie juste une collection vide, alors évidemment, renvoyer est ok. Toutefois, renvoyer une collection vide en tant que cas spécial en raison de paramètres non valides ou pour d'autres raisons n'est PAS une bonne idée, car il masque une condition de cas particulier.

En fait, dans ce cas, je préfère généralement lancer une exception pour m'assurer que ce n'est VRAIMENT pas ignoré :)

Dire que cela rend le code plus robuste (en renvoyant une collection vide), car ils n'ont pas à gérer la condition null est mauvais, car cela masque simplement un problème qui devrait être traité par le code appelant.

4
Larry Watanabe

Je dirais que 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, null n'est rien (sauf en SQL). Une collection vide est quelque chose, bien que 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 nulle. Mais il arrive parfois qu'une collection vide ne soit pas la même chose qu'une valeur nulle.

4
Jason Baker

Pensez toujours en faveur de vos clients (qui utilisent votre API):

Le renvoi de 'null' crée très souvent des problèmes avec les clients qui ne gèrent pas correctement les contrôles null, ce qui provoque une exception NullPointerException lors de l'exécution. J'ai vu des cas dans lesquels une telle vérification nulle manquante forçait un problème de production prioritaire (un client utilisait foreach (...) avec une valeur nulle). Lors du test, le problème ne s'est pas produit, car les données utilisées étaient légèrement différentes.

4
manuel aldana

J'aime donner expliquer 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);

Voici 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: comme AccountValue n'est pas null, la fonction Sum () ne renverra pas null. Par conséquent, je peux l'utiliser directement.

3

J'appelle cela mon erreur d'un milliard de dollars… À cette époque, je concevais le premier système de types complet pour les références dans un langage orienté objet. Mon objectif était de veiller à ce que toute utilisation des références soit absolument sûre, avec une vérification automatique par le compilateur. Mais je ne pouvais pas résister à la tentation de mettre une référence nulle, tout simplement parce que c'était si facile à mettre en œuvre. Cela a entraîné d'innombrables erreurs, vulnérabilités et pannes système, qui ont probablement causé des milliards 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 à propos de null en général. Je ne suis pas d'accord avec l'affirmation selon laquelle undefined est un autre null, mais cela vaut toujours la peine d'être lu. Et cela explique pourquoi vous devriez éviter null du tout et pas seulement dans le cas que vous avez demandé. L'essentiel est que null constitue dans toutes les langues un cas particulier. Vous devez penser à null comme une exception. undefined est différent en ce sens que 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 indéfini, mais la plupart d’entre elles n’ont pas d’identifiant pour cela dans la langue.

2
ceving

Nous avons eu cette discussion au sein de l’équipe de développement au travail il ya environ une semaine et nous avons presque unanimement opté pour une collecte vide. Une personne a voulu renvoyer null pour la même raison que Mike a spécifiée ci-dessus.

2
Henric

Collection vide. Si vous utilisez C #, l'hypothèse est que maximiser les ressources système n'est pas essentiel. Bien que moins efficace, renvoyer une collection vide est beaucoup plus pratique pour les programmeurs impliqués (pour la raison décrite ci-dessus).

2
mothis

Renvoyer une collection vide est préférable dans la plupart des cas.

La raison en est la facilité d'implémentation de l'appelant, un contrat cohérent et une implémentation plus facile.

Si une méthode renvoie null pour indiquer un résultat vide, l'appelant doit implémenter un adaptateur de contrôle de null en plus de l'énumération. Ce code est ensuite dupliqué dans différents appelants. Pourquoi ne pas insérer cet adaptateur dans la méthode afin de pouvoir le réutiliser?.

Une utilisation valide de null pour IEnumerable peut indiquer le résultat absent ou l'échec d'une opération, mais dans ce cas, il convient de prendre en compte d'autres techniques, telles que le lancement 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());
        }
    }
}
2
George Polevoy

Du point de vue de la gestion de la complexité, objectif principal de l’ingénierie logicielle, nous voulons éviter de propager la complexité non nécessaire cyclomatic aux clients d’une API. Renvoyer une valeur nulle au client revient à 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 collection vide.)

1
dthal