Est-il prudent d’accepter IEnumerable comme argument après l’introduction de Linq?

Quelques questions similaires à celle-ci concernent les bons types d’entrée et de sortie. Ma question porte plus sur les bonnes pratiques, la désignation des méthodes, le choix du type de paramètre, la protection contre les accidents, etc. à la suite de Linq .

Presque partout, Linq traite avec IEnumerable et ce n’est tout simplement pas cela, mais il introduit également quelque chose d’étranger à nous appelé exécution différée. Maintenant, nous avons peut-être commis une erreur en concevant nos méthodes (en particulier les méthodes d’extension) lorsque nous pensions que la meilleure idée est de prendre le type le plus fondamental. Donc, nos méthodes ressemblaient à:

 public static IEnumerable Shuffle(this IEnumerable lstObject) { foreach (T t in lstObject) //some fisher-yates may be } 

Le danger est évidemment lorsque nous combinons la fonction ci-dessus avec Linq paresseux et son si susceptible.

 var query = foos.Select(p => p).Where(p => p).OrderBy(p => p); //doesn't execute //but var query = foos.Select(p => p).Where(p => p).Shuffle().OrderBy(p => p); //the second line executes up to a point. 

Un gros assembly ici:

Pour clarifier la ligne ci-dessus – Ma question ne concerne pas la deuxième expression ne faisant pas l’object d’une évaluation, sérieusement. Les programmeurs le savent. Mon souci est que la méthode Shuffle exécute la requête jusqu’à ce point. Voir la première requête, où rien n’est exécuté. De la même façon, lors de la construction d’une autre expression Linq (qui devrait être exécutée plus tard), notre fonction personnalisée joue le spoilsport. En d’autres termes, comment faire savoir à l’appelant que Shuffle n’est pas le type de fonction qu’il voudrait à ce stade de l’expression Linq. J’espère que le point est ramené à la maison. Excuses! 🙂 Bien que ce soit aussi simple que d’aller inspecter la méthode, je vous demande comment vous programmez typiquement de manière défensive.

L’exemple ci-dessus n’est peut-être pas dangereux, mais vous comprenez. C’est certain que les fonctions (personnalisées) ne vont pas bien avec l’idée de Linq d’exécution différée. Le problème ne concerne pas seulement les performances, mais également les effets secondaires inattendus.

Mais une fonction comme celle-ci fonctionne Linq avec Linq :

 public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) { HashSet seenKeys = new HashSet(); //credits Jon Skeet foreach (var element in source) if (seenKeys.Add(keySelector(element))) yield return element; } 

Comme vous pouvez le constater, les fonctions prennent IEnumerable , mais l’appelant ne sait pas comment les fonctions réagissent. Alors, quelles sont les mesures de précaution générales que vous prenez ici?

  1. Nommez nos méthodes personnalisées de manière appropriée pour donner à l’appelant l’idée qu’il présage ou non de Linq ?

  2. Déplacez les méthodes paresseuses vers un espace de noms différent, et conservez Linq -ish dans un autre, afin que cela donne au moins une idée?

  3. N’acceptez pas un IEnumerable tant que paramètre pour immediately exécution immediately méthodes, mais utilisez plutôt un type plus dérivé ou un type concret lui-même, ce qui laisse IEnumerable uniquement pour les méthodes paresseuses? Cela met la charge de l’appelant sur l’exécution d’éventuelles expressions non exécutées? C’est tout à fait possible pour nous, car en dehors du monde Linq nous n’avons pratiquement pas à traiter avec IEnumerable , et la plupart des classes de collection de base s’implémentent jusqu’à ICollection .

Ou autre chose? J’aime particulièrement la 3ème option, et c’est ce que je voulais faire, mais je pensais avoir vos idées avant. J’ai vu beaucoup de code (de jolies petites méthodes semblables à Linq !), Même de bons programmeurs qui acceptent IEnumerable et font une liste ToList() ou quelque chose de similaire dans la méthode. Je ne sais pas comment ils gèrent les effets secondaires.

Edit: Après un vote négatif et une réponse, je voudrais préciser que ce n’est pas que les programmeurs ne sachent pas comment fonctionne Linq (notre compétence pourrait être à un certain niveau, mais c’est différent), mais c’est que beaucoup de fonctions ont été écrites sans prendre Linq en compte à l’époque. Maintenant, le chaînage d’une méthode d’exécution immédiate et de méthodes d’extension Linq rend cette opération dangereuse. Ma question est donc la suivante: existe-t-il une directive générale à suivre par les programmeurs pour que l’appelant sache quoi utiliser du côté de Linq et quoi ne pas utiliser? Il s’agit plus de programmer de manière défensive que de ne pas savoir si vous ne savez pas l’utiliser, nous ne pouvons pas vous aider! (ou du moins je crois) ..

Comme vous pouvez le constater, les fonctions prennent IEnumerable<> , mais l’appelant ne sait pas comment les fonctions réagissent.

C’est simplement une question de documentation. Consultez la documentation de DistinctBy dans MoreLINQ , qui comprend:

Cet opérateur utilise une exécution différée et transmet les résultats en continu, même si un ensemble de clés déjà vues est conservé. Si une clé est vue plusieurs fois, seul le premier élément avec cette clé est renvoyé.

Oui, il est important de savoir ce que fait un membre avant de l’utiliser, et pour ce qui est d’accepter ou de retourner une collection, il y a plusieurs choses importantes à savoir:

  • La collection sera-t-elle lue immédiatement ou différée?
  • La collection sera-t-elle diffusée pendant le retour des résultats?
  • Si le type de collection déclaré accepté est modifiable, la méthode tentera-t-elle de le muter?
  • Si le type de collection déclaré renvoyé est mutable, s’agira-t-il d’une implémentation mutable?
  • La collection renvoyée sera-t-elle modifiée par d’autres actions (par exemple, s’agit-il d’une vue en lecture seule d’une collection pouvant être modifiée au sein de la classe)?
  • Est-ce que null est une valeur d’entrée acceptable?
  • La valeur null est-elle une valeur d’ élément acceptable?
  • La méthode retournera-t-elle jamais null ?

Toutes ces choses méritent d’être examinées – et la plupart d’entre elles l’étaient bien avant LINQ.

La morale est vraiment: “Assurez-vous de savoir comment quelque chose se comporte avant de l’appeler.” C’était vrai avant LINQ, et LINQ ne l’a pas changé. Il vient d’introduire deux possibilités (exécution différée et diffusion en continu) qui étaient rarement présentes auparavant.

Utilisez IEnumerable partout où cela convient, et codez de manière défensive .

Comme SLaks l’a souligné dans un commentaire, l’exécution différée est possible avec IEnumerable depuis le début et depuis que C # 2.0 a introduit la déclaration de yield , il est très facile de mettre en œuvre l’exécution différée vous-même. Par exemple, cette méthode retourne un IEnumerable qui utilise une exécution différée pour renvoyer des nombres aléatoires:

 public static IEnumerable RandomSequence(int length) { Random rng = new Random(); for (int i = 0; i < length; i++) { Console.WriteLine("deferred execution!"); yield return rng.Next(); } } 

Ainsi, chaque fois que vous utilisez foreach pour boucler un IEnumerable, vous devez supposer que tout peut arriver entre les itérations. Il pourrait même renvoyer une exception, vous pouvez donc placer la boucle foreach dans un try/finally .

Si l'appelant passe un IEnumerable qui fait quelque chose de dangereux ou ne cesse jamais de renvoyer des numéros (une séquence infinie), ce n'est pas votre faute . Vous n'avez pas à le détecter et à lancer une erreur; Il suffit d’append suffisamment de gestionnaires d’exception pour que votre méthode puisse se nettoyer en cas de problème. Dans le cas de quelque chose d'aussi simple que Shuffle , il n'y a rien à faire; laissez simplement l’appelant traiter l’exception.

Dans les rares cas où votre méthode ne peut vraiment pas traiter une séquence infinie, envisagez d'accepter un type différent, tel que IList . Mais même IList ne vous protégera pas des exécutions différées - vous ne savez pas quelle classe implémente IList ou quelle sorte de vaudou faire pour afficher chaque élément! Dans le cas très rare où vous ne pouvez vraiment pas autoriser l' exécution de code inattendu pendant une itération, vous devriez accepter un tableau , pas n'importe quel type d'interface.

L’exécution différée n’a rien à voir avec les types. Toute méthode linq utilisant des iterators peut être différée si vous écrivez votre code de cette façon. Select() , Where() , OrderByDescending() pour, par exemple, tous les iterators et retardent donc l’exécution. Oui, ces méthodes s’attendent à un IEnumerable , mais cela ne signifie pas que IEnumerable est le problème.

C’est certain que les fonctions (personnalisées) ne vont pas bien avec l’idée de Linq d’exécution différée. Le problème ne concerne pas seulement les performances, mais également les effets secondaires inattendus.

Alors, quelles sont les mesures de précaution générales que vous prenez ici?

Aucun. Honnêtement, nous utilisons IEnumerable partout et n’avons pas le problème des personnes qui ne comprennent pas les “effets secondaires”. “l’idée de l’exécution différée sous Linq” est essentielle à son utilité dans des domaines comme Linq-to-SQL. Il me semble que la conception des fonctions personnalisées n’est pas aussi claire qu’elle pourrait l’être. Si des personnes écrivent du code pour utiliser LINQ et ne comprennent pas ce qu’il fait, c’est le problème, pas le fait que IEnumerable se trouve être un type de base.

Toutes vos idées ne font que décrire le fait qu’il semble que certains programmeurs ne comprennent tout simplement pas les requêtes linq. Si vous n’avez pas besoin d’une exécution paresseuse, ce qui semble être le cas, forcez tout pour qu’il soit évalué avant la sortie des fonctions. Appelez ToList () sur vos résultats et renvoyez-les dans une API cohérente avec laquelle le consommateur aimerait travailler – listes, tableaux, collections ou IEnumerables.