Je développe une bibliothèque destinée à une diffusion publique. Il contient différentes méthodes pour opérer sur des ensembles d'objets - générer, inspecter, partitionner et projeter les ensembles dans de nouveaux formulaires. Dans le cas où cela est pertinent, il s'agit d'une bibliothèque de classes C # contenant des extensions de style LINQ sur IEnumerable
, à publier sous forme de package NuGet.
Certaines des méthodes de cette bibliothèque peuvent recevoir des paramètres d'entrée insatisfaisants. Par exemple, dans les méthodes combinatoires, il existe une méthode pour générer tous les ensembles d'éléments n qui peuvent être construits à partir d'un ensemble source d'éléments m. Par exemple, étant donné l'ensemble:
1, 2, 3, 4, 5
et demander des combinaisons de 2 produirait:
1, 2
1, 3
1, 4
etc...
5, 3
5, 4
Maintenant, il est évidemment possible de demander quelque chose qui ne peut pas être fait, comme lui donner un ensemble de 3 éléments, puis demander des combinaisons de 4 éléments tout en définissant l'option qui dit qu'il ne peut utiliser chaque élément qu'une seule fois.
Dans ce scénario, chaque paramètre est individuellement valide:
Cependant, l'état des paramètres pris ensemble cause des problèmes.
Dans ce scénario, vous attendriez-vous à ce que la méthode lève une exception (par exemple, InvalidOperationException
), ou renvoie une collection vide? Soit me semble valable:
InvalidOperationException
.Ma première préoccupation est qu'une exception empêche le chaînage idiomatique de style LINQ lorsque vous traitez des ensembles de données dont la taille est inconnue. En d'autres termes, vous voudrez peut-être faire quelque chose comme ceci:
var result = someInputSet
.CombinationsOf(4, CombinationsGenerationMode.Distinct)
.Select(combo => /* do some operation to a combination */)
.ToList();
Si votre jeu d'entrée est de taille variable, le comportement de ce code est imprévisible. Si .CombinationsOf()
lève une exception lorsque someInputSet
a moins de 4 éléments, alors ce code parfois échouera à l'exécution sans quelques vérifications préalables. Dans l'exemple ci-dessus, cette vérification est triviale, mais si vous l'appelez à mi-chemin d'une longue chaîne de LINQ, cela peut devenir fastidieux. S'il renvoie un ensemble vide, alors result
sera vide, ce dont vous serez parfaitement satisfait.
Ma deuxième préoccupation est que le retour d'un ensemble vide peut masquer des problèmes - si vous appelez cette méthode à mi-chemin d'une chaîne de LINQ et qu'elle retourne tranquillement un ensemble vide, vous pouvez rencontrer des problèmes quelques étapes plus tard, ou vous retrouver avec un vide jeu de résultats, et il peut ne pas être évident comment cela s'est produit étant donné que vous aviez certainement quelque chose dans l'ensemble d'entrée.
À quoi vous attendriez-vous et quel est votre argument?
Renvoyer un ensemble vide
Je m'attendrais à un ensemble vide parce que:
Il y a 0 combinaisons de 4 nombres de l'ensemble de 3 quand je ne peux utiliser chaque nombre qu'une seule fois
En cas de doute, demandez à quelqu'un d'autre.
Votre exemple de fonction en a une très similaire en Python: itertools.combinations
. Voyons voir comment ça fonctionne:
>>> import itertools
>>> input = [1, 2, 3, 4, 5]
>>> list(itertools.combinations(input, 2))
[(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]
>>> list(itertools.combinations(input, 5))
[(1, 2, 3, 4, 5)]
>>> list(itertools.combinations(input, 6))
[]
Et ça me va parfaitement bien. Je m'attendais à un résultat sur lequel je pourrais répéter et j'en ai eu un.
Mais, évidemment, si vous deviez demander quelque chose de stupide:
>>> list(itertools.combinations(input, -1))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: r must be non-negative
Je dirais donc que si tous vos paramètres valident mais que le résultat est un ensemble vide renvoyant un ensemble vide, vous n'êtes pas le seul à le faire.
Comme l'a dit @ Bakuri dans les commentaires, c'est également la même chose pour une requête SQL
comme SELECT <columns> FROM <table> WHERE <conditions>
. Aussi longtemps que <columns>
, <table>
, <conditions>
sont bien formés et font référence à des noms existants, vous pouvez créer un ensemble de conditions qui s'excluent mutuellement. La requête résultante ne produirait simplement aucune ligne au lieu de lancer un InvalidConditionsError
.
En termes simples:
Je suis d'accord avec réponse d'Ewan mais je veux ajouter un raisonnement spécifique.
Vous avez affaire à des opérations mathématiques, il peut donc être judicieux de s'en tenir aux mêmes définitions mathématiques. D'un point de vue mathématique, le nombre de r - ensembles d'un n - set (ie nCr ) est bien défini pour tout r> n> = 0. Il est nul. Par conséquent, le retour d'un ensemble vide serait le cas attendu d'un point de vue mathématique.
Je trouve qu'un bon moyen de déterminer s'il faut utiliser une exception, c'est d'imaginer des gens impliqués dans la transaction.
Prenons l'exemple de la récupération du contenu d'un fichier:
Veuillez me récupérer le contenu du fichier, "n'existe pas.txt"
une. "Voici le contenu: une collection vide de personnages"
b. "Euh, il y a un problème, ce fichier n'existe pas. Je ne sais pas quoi faire!"
Veuillez me récupérer le contenu du fichier, "existe mais est vide.txt"
une. "Voici le contenu: une collection vide de personnages"
b. "Euh, il y a un problème, il n'y a rien dans ce fichier. Je ne sais pas quoi faire!"
Sans doute, certains seront en désaccord, mais pour la plupart des gens, "Euh, il y a un problème" est logique lorsque le fichier n'existe pas et renvoie "une collection de caractères vide" lorsque le fichier est vide.
Donc, appliquez la même approche à votre exemple:
Veuillez me donner toutes les combinaisons de 4 articles pour {1, 2, 3}
une. Il n'y en a pas, voici un ensemble vide.
b. Il y a un problème, je ne sais pas quoi faire.
Encore une fois, "Il y a un problème" aurait du sens si par exemple null
était proposé comme ensemble d'éléments, mais "voici un ensemble vide" semble une réponse sensée à la demande ci-dessus.
Si le retour d'une valeur vide masque un problème (par exemple un fichier manquant, un null
), une exception doit généralement être utilisée à la place (sauf si la langue choisie prend en charge option/maybe
types, alors ils ont parfois plus de sens). Sinon, le retour d'une valeur vide simplifiera probablement le coût et sera mieux conforme au principe du moindre étonnement.
Comme c'est pour une bibliothèque à usage général, mon instinct serait Laissez l'utilisateur final choisir.
Tout comme nous avons Parse()
et TryParse()
à notre disposition, nous pouvons avoir l'option dont nous utilisons en fonction de la sortie dont nous avons besoin de la fonction. Vous passeriez moins de temps à écrire et à maintenir un wrapper de fonction pour lever l'exception que de vous disputer pour choisir une seule version de la fonction.
Vous devez valider les arguments fournis lors de l'appel de votre fonction. Et en fait, vous voulez savoir comment gérer les arguments non valides. Le fait que plusieurs arguments dépendent les uns des autres ne compense pas le fait que vous validez les arguments.
Ainsi, je voterais pour l'ArgumentException fournissant les informations nécessaires pour que l'utilisateur comprenne ce qui s'est mal passé.
Par exemple, vérifiez la fonction public static TSource ElementAt<TSource>(this IEnumerable<TSource>, Int32)
dans Linq. Ce qui lève une ArgumentOutOfRangeException si l'index est inférieur à 0 ou supérieur ou égal au nombre d'éléments dans la source. Ainsi, l'index est validé par rapport à l'énumérable fourni par l'appelant.
Vous devez effectuer l'une des opérations suivantes (tout en continuant à mettre constamment sur les problèmes de base tels qu'un nombre négatif de combinaisons):
Fournissez deux implémentations, l'une qui renvoie un ensemble vide lorsque les entrées ensemble sont absurdes et l'autre qui lance. Essayez de les appeler CombinationsOf
et CombinationsOfWithInputCheck
. Ou tout ce que vous aimez. Vous pouvez inverser cela pour que celui qui vérifie les entrées soit le nom le plus court et celui de la liste soit CombinationsOfAllowInconsistentParameters
.
Pour les méthodes Linq, renvoyez le IEnumerable
vide sur la prémisse exacte que vous avez décrite. Ensuite, ajoutez ces méthodes Linq à votre bibliothèque:
public static class EnumerableExtensions {
public static IEnumerable<T> ThrowIfEmpty<T>(this IEnumerable<T> input) {
return input.IfEmpty<T>(() => {
throw new InvalidOperationException("An enumerable was unexpectedly empty");
});
}
public static IEnumerable<T> IfEmpty<T>(
this IEnumerable<T> input,
Action callbackIfEmpty
) {
var enumerator = input.GetEnumerator();
if (!enumerator.MoveNext()) {
// Safe because if this throws, we'll never run the return statement below
callbackIfEmpty();
}
return EnumeratePrimedEnumerator(enumerator);
}
private static IEnumerable<T> EnumeratePrimedEnumerator<T>(
IEnumerator<T> primedEnumerator
) {
yield return primedEnumerator.Current;
while (primedEnumerator.MoveNext()) {
yield return primedEnumerator.Current;
}
}
}
Enfin, utilisez-le comme ceci:
var result = someInputSet
.CombinationsOf(4, CombinationsGenerationMode.Distinct)
.ThrowIfEmpty()
.Select(combo => /* do some operation to a combination */)
.ToList();
ou comme ça:
var result = someInputSet
.CombinationsOf(4, CombinationsGenerationMode.Distinct)
.IfEmpty(() => _log.Warning(
$@"Unexpectedly received no results when creating combinations for {
nameof(someInputSet)}"
))
.Select(combo => /* do some operation to a combination */)
.ToList();
Veuillez noter que la méthode privée étant différente des méthodes publiques est requise pour que le comportement de lancement ou d'action se produise lorsque la chaîne linq est créée au lieu d'un certain temps plus tard lorsqu'elle est énumérée. Vous voulez le jeter immédiatement.
Notez, cependant, qu'il doit bien sûr énumérer au moins le premier élément afin de déterminer s'il existe des éléments. C'est un inconvénient potentiel qui, je pense, est principalement atténué par le fait que les futurs téléspectateurs peuvent assez facilement raisonner qu'une méthode ThrowIfEmpty
has pour énumérer au moins un élément, donc ne pas être surpris de le faire. Mais tu ne sais jamais. Vous pourriez rendre cela plus explicite ThrowIfEmptyByEnumeratingAndReEmittingFirstItem
. Mais cela semble être une gigantesque exagération.
Je pense que le n ° 2 est vraiment génial! Maintenant, le pouvoir est dans le code appelant, et le prochain lecteur du code comprendra exactement ce qu'il fait et n'aura pas à faire face à des exceptions inattendues.
Je peux voir des arguments pour les deux cas d'utilisation - une exception est excellente si le code en aval attend des ensembles qui contiennent des données. D'un autre côté, un ensemble vide est tout simplement génial si cela est prévu.
Je pense que cela dépend des attentes de l'appelant s'il s'agit d'une erreur ou d'un résultat acceptable - je transférerais donc le choix à l'appelant. Peut-être introduire une option?
.CombinationsOf(4, CombinationsGenerationMode.Distinct, Options.AllowEmptySets)
Il existe deux approches pour décider s'il n'y a pas de réponse évidente:
Écrivez le code en supposant d'abord une option, puis l'autre. Déterminez lequel fonctionnerait le mieux dans la pratique.
Ajoutez un paramètre booléen "strict" pour indiquer si vous souhaitez que les paramètres soient strictement vérifiés ou non. Par exemple, Java SimpleDateFormat
a une méthode setLenient
pour tenter d'analyser les entrées qui ne correspondent pas entièrement au format. Bien sûr, vous devez décider quelle est la valeur par défaut.
Sur la base de votre propre analyse, le retour de l'ensemble vide semble clairement correct - vous l'avez même identifié comme quelque chose que certains utilisateurs peuvent réellement vouloir et ne sont pas tombés dans le piège d'interdire une utilisation parce que vous ne pouvez pas imaginer que les utilisateurs souhaitent jamais l'utiliser de cette façon.
Si vous pensez vraiment que certains utilisateurs peuvent vouloir forcer les retours non vides, alors donnez-leur un moyen de demander pour ce comportement plutôt que de l'imposer à tout le monde. Par exemple, vous pourriez:
AssertNonempty
contrôle qu'ils peuvent mettre dans leurs chaînes.Cela dépend vraiment de ce que vos utilisateurs attendent. Pour un exemple (quelque peu indépendant) si votre code effectue une division, vous pouvez soit lever une exception ou retourner Inf
ou NaN
lorsque vous divisez par zéro. Mais ce n'est ni bon ni mauvais:
Inf
dans une bibliothèque Python, les gens vous attaqueront pour cacher des erreursDans votre cas, je choisirais la solution qui sera la moins étonnante pour les utilisateurs finaux. Puisque vous développez une bibliothèque traitant des ensembles, un ensemble vide semble être quelque chose que vos utilisateurs s'attendent à traiter, donc le renvoyer semble une chose sensée à faire. Mais je peux me tromper: vous avez une bien meilleure compréhension du contexte que quiconque ici, donc si vous vous attendez à ce que vos utilisateurs comptent sur le fait que l'ensemble n'est pas toujours vide, vous devez immédiatement lever une exception.
Les solutions qui laisser l'utilisateur choisir (comme l'ajout d'un paramètre "strict") ne sont pas définitives, car elles remplacent la question d'origine par une nouvelle équivalente: "Quelle valeur de strict
devrait être la valeur par défaut? "
Il est courant (en mathématiques) que lorsque vous sélectionnez des éléments sur un ensemble , vous ne puissiez trouver aucun élément et donc vous obtenez un ensemble vide . Bien sûr, vous devez être cohérent avec les mathématiques si vous procédez comme suit:
Règles d'ensemble communes:
Votre question est très subtile:
Il se pourrait que l'entrée de votre fonction ait pour respecter un contrat : Dans ce cas, toute entrée invalide devrait déclencher une exception, ça y est, la fonction ne fonctionne pas avec des paramètres réguliers.
Il se pourrait que l'entrée de votre fonction doive se comporter exactement comme un ensemble , et devrait donc pouvoir retourner un ensemble vide.
Maintenant, si j'étais en vous, j'irais dans le sens "Set", mais avec un gros "BUT".
Supposons que vous ayez une collection qui "par hypotesis" ne devrait avoir que des étudiantes:
class FemaleClass{
FemaleStudent GetAnyFemale(){
var femaleSet= mySet.Select( x=> x.IsFemale());
if(femaleSet.IsEmpty())
throw new Exception("No female students");
else
return femaleSet.Any();
}
}
Maintenant, votre collection n'est plus un "ensemble pur", car vous avez un contrat dessus, et donc vous devez faire respecter votre contrat avec une exception.
Lorsque vous utilisez vos fonctions "set" de manière pure, vous ne devez pas lever d'exceptions en cas de set vide, mais si vous avez des collections qui ne sont plus des "sets purs", vous devez lever des exceptions le cas échéant .
Vous devez toujours faire ce qui vous semble le plus naturel et le plus cohérent: pour moi, un ensemble doit adhérer à des règles définies, tandis que les choses qui ne le sont pas doivent avoir leurs règles correctement pensées.
Dans votre cas, cela semble une bonne idée de faire:
List SomeMethod( Set someInputSet){
var result = someInputSet
.CombinationsOf(4, CombinationsGenerationMode.Distinct)
.Select(combo => /* do some operation to a combination */)
.ToList();
// the only information here is that set is empty => there are no combinations
// BEWARE! if 0 here it may be invalid input, but also a empty set
if(result.Count == 0) //Add: "&&someInputSet.NotEmpty()"
// we go a step further, our API require combinations, so
// this method cannot satisfy the API request, then we throw.
throw new Exception("you requsted impossible combinations");
return result;
}
Mais ce n'est pas vraiment une bonne idée, nous avons maintenant un état non valide qui peut se produire au moment de l'exécution à des moments aléatoires, mais cela est implicite dans le problème, nous ne pouvons donc pas le supprimer, bien sûr, nous pouvons déplacer l'exception dans certains méthode utilitaire (c'est exactement le même code, déplacé à différents endroits), mais c'est faux et fondamentalement la meilleure chose que vous pouvez faire est de coller aux règles établies régulièrement .
En fait, l'ajout d'une nouvelle complexité juste pour montrer que vous pouvez écrire des méthodes de requêtes linq semble ne vaut pas pour votre problème , je suis presque sûr que si OP peut nous en dire plus à propos de son domaine, nous pourrions probablement trouver l'endroit où l'exception est vraiment nécessaire (le cas échéant, il est possible que le problème ne nécessite aucune exception).