web-dev-qa-db-fra.com

Dois-je accepter des collections vides dans mes méthodes qui les parcourent?

J'ai une méthode où toute la logique est effectuée à l'intérieur d'une boucle foreach qui itère sur le paramètre de la méthode:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

Dans ce cas, l'envoi d'une collection vide entraîne une collection vide, mais je me demande si ce n'est pas judicieux.

Ma logique ici est que si quelqu'un appelle cette méthode, il a l'intention de transmettre des données et ne transmettra une collection vide à ma méthode que dans des circonstances erronées.

Dois-je intercepter ce comportement et lever une exception pour cela, ou est-il recommandé de renvoyer la collection vide?

40
Nick Udell

Les méthodes utilitaires ne doivent pas jeter sur des collections vides. Vos clients API vous détesteraient pour cela.

Une collection peut être vide; une "collection-qui-ne-doit-pas-être-vide" est conceptuellement une chose beaucoup plus difficile à travailler.

Transformer une collection vide a un résultat évident: la collection vide. (Vous pouvez même économiser des ordures en renvoyant le paramètre lui-même.)

Il existe de nombreuses circonstances dans lesquelles un module maintient des listes de choses qui peuvent ou non déjà être remplies de quelque chose. Devoir vérifier le vide avant chaque appel à transform est ennuyeux et a le potentiel de transformer un algorithme simple et élégant en un désordre laid.

Les méthodes d'utilité devraient toujours s'efforcer d'être libérales dans leurs intrants et conservatrices dans leurs extrants.

Pour toutes ces raisons, pour l'amour de Dieu, manipulez correctement la collection vide. Rien n'est plus exaspérant qu'un module d'aide qui pense il sait ce que vous voulez mieux que vous.

175
Kilian Foth

Je vois deux questions importantes qui déterminent la réponse à cela:

  1. Votre fonction peut-elle renvoyer quelque chose de significatif et de logique lorsqu'elle est transmise à une collection vide (y compris null )?
  2. Quel est le style de programmation général dans cette application/bibliothèque/équipe? (Plus précisément, comment allez-vous FP?)

1. Retour significatif

Essentiellement, si vous pouvez renvoyer quelque chose de significatif, ne lancez pas d'exception. Laissez l'appelant gérer le résultat. Donc, si votre fonction ...

  • Compte le nombre d'éléments dans la collection, retourne 0. C'était facile.
  • Recherche des éléments qui correspondent à des critères particuliers, retourne une collection vide. Veuillez ne rien jeter. L'appelant peut avoir une énorme collection de collections, dont certaines sont vides, d'autres non. L'appelant veut tous les éléments correspondants dans n'importe quelle collection. Les exceptions ne font que rendre la vie de l'appelant plus difficile.
  • Recherche les critères les plus grands/les plus petits/les mieux adaptés aux critères de la liste. Oops. Selon la question de style, vous pouvez lever une exception ici ou renvoyer null . Je déteste null (très bien une personne FP personne) mais c'est sans doute plus significatif ici et il vous permet de réserver des exceptions pour des erreurs inattendues dans votre propre code. Si l'appelant ne vérifie pas null, une exception assez claire en résultera de toute façon. Mais vous leur laissez cela.
  • Demande le nième élément de la collection ou le premier/dernier n éléments. C'est encore le meilleur cas pour une exception et celui qui est le moins susceptible de créer de la confusion et des difficultés pour l'appelant. Un cas peut toujours être fait pour null si vous et votre équipe avez l'habitude de vérifier cela, pour toutes les raisons données au point précédent, mais ceci est le cas le plus valable à ce jour pour lever une DudeYou Know YouShouldCheckTheSizeFirst exception. Si votre style est plus fonctionnel, alors null ou lisez mon Style réponse.

En général, mon biais FP me dit "retourne quelque chose de significatif" et null peut avoir une signification valide dans ce cas.

2. Style

Votre code général (ou le code projet ou le code équipe) privilégie-t-il un style fonctionnel? Si non, des exceptions seront attendues et traitées. Si oui, envisagez de renvoyer un Type d'option . Avec un type d'option, vous retournez une réponse significative ou Aucun/Rien . Dans mon troisième exemple ci-dessus, Rien serait une bonne réponse dans le style FP. Le fait que la fonction renvoie un type d'option signale clairement à l'appelant qu'une réponse significative peut ne pas être possible et que l'appelant doit être prêt à y faire face. Je pense que cela donne à l'appelant plus d'options (si vous pardonnez le jeu de mots).

F # est l'endroit où tous les enfants cool de .Net font ce genre de chose, mais C # le faitsupport ce style.

tl; dr

Conservez des exceptions pour les erreurs inattendues dans votre propre chemin de code, pas entièrement prévisible (et légale) entrée de quelqu'un d'autre.

26
itsbruce

Comme toujours, cela dépend.

Est-ce matière que la collection est vide?
La plupart des codes de gestion des collections diraient probablement "non"; une collection peut contenir n'importe quel nombre éléments, y compris zéro.

Maintenant, si vous avez une sorte de collection où il est "invalide" de ne pas avoir d'articles, alors c'est une nouvelle exigence et vous devez décider quoi faire à ce sujet.

Empruntez une logique de test au monde de la base de données: testez les éléments zéro, n et deux les éléments. Cela répond aux cas les plus importants (éliminant toute condition de jointure interne ou cartésienne mal formée).

18
Phill W.

Pour une bonne conception, acceptez autant de variations dans vos entrées que possible ou possible. Les exceptions doivent uniquement être levées lorsque (une entrée inacceptable est présentée OR des erreurs inattendues se produisent lors du traitement) ET le programme ne peut pas continuer de manière prévisible en conséquence.

Dans ce cas, il faut s'attendre à ce qu'une collection vide soit présentée, et votre code doit la gérer (ce qu'elle fait déjà). Ce serait une violation de tout ce qui est bon si votre code levait une exception ici. Cela reviendrait à multiplier 0 par 0 en mathématiques. C'est redondant, mais absolument nécessaire pour qu'il fonctionne comme il le fait.

Maintenant, passons à l'argument de collection null. Dans ce cas, une collection null est une erreur de programmation: le programmeur a oublié d'assigner une variable. Il s'agit d'un cas où une exception pourrait être levée, car vous ne pouvez pas traiter cela de manière significative dans une sortie, et tenter de le faire introduirait un comportement inattendu. Cela reviendrait à diviser par zéro en mathématiques - cela n'a aucun sens.

11
theMayer

La bonne solution est beaucoup plus difficile à voir lorsque vous examinez votre fonction isolément. Considérez votre fonction comme une partie de n problème plus important . Une solution possible à cet exemple ressemble à ceci (dans Scala):

input.split("\\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

Tout d'abord, vous divisez la chaîne par des non-chiffres, filtrez les chaînes vides, convertissez les chaînes en entiers, puis filtrez pour ne conserver que les nombres à quatre chiffres. Votre fonction peut être la map (_.toInt) dans le pipeline.

Ce code est assez simple car chaque étape du pipeline gère simplement une chaîne vide ou une collection vide. Si vous mettez une chaîne vide au début, vous obtenez une liste vide à la fin. Vous n'avez pas besoin de vous arrêter et de rechercher null ou une exception après chaque appel.

Bien sûr, cela suppose qu'une liste de sortie vide n'a pas plus d'une signification. Si vous avez besoin de faire la différence entre une sortie vide provoquée par une entrée vide et une causée par la transformation elle-même, cela change complètement les choses.

10
Karl Bielefeldt

Cette question concerne en fait les exceptions. Si vous le regardez de cette façon et ignorez la collection vide en tant que détail d'implémentation, la réponse est simple:

1) Une méthode doit lever une exception lorsqu'elle ne peut pas continuer: soit incapable de faire la tâche désignée, soit renvoyer la valeur appropriée.

2) Une méthode doit intercepter une exception lorsqu'elle est en mesure de continuer malgré l'échec.

Ainsi, votre méthode d'assistance ne doit pas être "utile" et lever une exception à moins que it ne soit pas en mesure de faire son travail avec une collection vide. Laissez l'appelant déterminer si les résultats peuvent être traités.

Qu'il retourne une collection vide ou null, c'est un peu plus difficile mais pas beaucoup: les collections nullables doivent être évitées si possible. Le but d'une collection nullable serait d'indiquer (comme dans SQL) que vous n'avez pas les informations - une collection d'enfants par exemple pourrait être nulle si vous ne savez pas si quelqu'un en a, mais vous n'en avez pas sachez que non. Mais si cela est important pour une raison quelconque, cela vaut probablement une variable supplémentaire pour le suivre.

2
jmoreno

La méthode est nommée TransformNodes. Dans le cas d'une collection vide en entrée, récupérer une collection vide est naturel et intuitif, et a un sens mathématique parfait.

Si la méthode était nommée Max et conçue pour renvoyer l'élément maximum, alors il serait naturel de lancer NoSuchElementException sur une collection vide, car le maximum de rien n'a aucun sens mathématique.

Si la méthode était nommée JoinSqlColumnNames et conçue pour renvoyer une chaîne où les éléments sont joints par une virgule à utiliser dans les requêtes SQL, il serait alors judicieux de lancer IllegalArgumentException sur une collection vide, comme l'appelant obtiendrait finalement une erreur SQL de toute façon s'il utilisait la chaîne dans une requête SQL directement sans autre vérification, et au lieu de rechercher une chaîne vide retournée, il aurait vraiment dû vérifier la collection vide à la place.

1
janos

Revenons en arrière et utilisons un exemple différent qui calcule la moyenne arithmétique d'un tableau de valeurs.

Si le tableau d'entrée est vide (ou nul), pouvez-vous raisonnablement répondre à la demande de l'appelant? Quelles sont vos options? Eh bien, vous pourriez:

  • présenter/retourner/jeter une erreur. en utilisant la convention de votre base de code pour cette classe d'erreur.
  • documenter qu'une valeur telle que zéro sera retournée
  • document qu'une valeur invalide désignée sera retournée (par exemple NaN)
  • documenter qu'une valeur magique sera retournée (par exemple, un min ou un max pour le type ou une valeur indicative, espérons-le)
  • déclarer que le résultat n'est pas spécifié
  • déclarer que l'action n'est pas définie
  • etc.

Je dis leur donner l'erreur s'ils vous ont donné une entrée invalide et la demande ne peut pas être traitée. Je veux dire une erreur grave dès le premier jour afin qu'ils comprennent les exigences de votre programme. Après tout, votre fonction n'est pas en mesure de répondre. Si l'opération pourrait échouer (par exemple, copier un fichier), votre API devrait leur donner une erreur à laquelle ils peuvent faire face.

Cela peut donc définir comment votre bibliothèque gère les demandes mal formées et les demandes qui peuvent échouer.

Il est très important que votre code soit cohérent dans la façon dont il gère ces classes d'erreurs.

La catégorie suivante consiste à décider de la manière dont votre bibliothèque gère les requêtes non-sens. Revenons à un exemple similaire au vôtre - utilisons une fonction qui détermine si un fichier existe sur un chemin: bool FileExistsAtPath(String). Si le client transmet une chaîne vide, comment gérez-vous ce scénario? Que diriez-vous d'un tableau vide ou nul passé à void SaveDocuments(Array<Document>)? Décidez de votre bibliothèque/base de code et soyez cohérent. Il se trouve que je considère ces cas comme des erreurs et j'interdis aux clients de faire des demandes non-sens en les signalant comme des erreurs (via une assertion). Certaines personnes résisteront fortement à cette idée/action. Je trouve cette détection d'erreur très utile. C'est très bon pour localiser les problèmes dans les programmes - avec une bonne localisation par rapport au programme incriminé. Les programmes sont beaucoup plus clairs et corrects (pensez à l'évolution de votre base de code), et ne gravez pas de cycles dans des fonctions qui ne font rien. Le code est plus petit/plus propre de cette façon, et les contrôles sont généralement poussés vers les emplacements où le problème peut être introduit.

0
justin