Avertissement de gestion pour une énumération multiple possible de IEnumerable

Dans mon code en besoin d’utiliser un IEnumerable plusieurs fois obtenir ainsi l’erreur Resharper de “Multiple énumération possible de IEnumerable “.

Exemple de code:

 public List Foo(IEnumerable objects) { if (objects == null || !objects.Any()) throw new ArgumentException(); var firstObject = objects.First(); var list = DoSomeThing(firstObject); var secondList = DoSomeThingElse(objects); list.AddRange(secondList); return list; } 
  • Je peux changer le paramètre d’ objects pour qu’il soit List et éviter ensuite l’énumération multiple possible, mais alors je ne reçois pas l’object le plus élevé que je puisse gérer.
  • Une autre chose que je peux faire est de convertir le IEnumerable en List au début de la méthode:

  public List Foo(IEnumerable objects) { var objectList = objects.ToList(); // ... } 

Mais c’est juste gênant .

Que feriez-vous dans ce scénario?

Le problème de prendre IEnumerable en paramètre est d’indiquer aux appelants “Je souhaite énumérer ceci”. Cela ne leur dit pas combien de fois vous souhaitez énumérer.

Je peux changer le paramètre d’objects pour qu’il soit List et éviter ensuite l’énumération multiple possible, mais alors je ne reçois pas l’object le plus élevé que je puisse gérer .

Le but de prendre l’object le plus élevé est noble, mais laisse place à trop d’hypothèses. Voulez-vous vraiment que quelqu’un transmette une requête LINQ to SQL à cette méthode, uniquement pour que vous puissiez l’énumérer deux fois (obtenir des résultats potentiellement différents à chaque fois?)

La sémantique qui manque ici est qu’un appelant, qui ne prend peut-être pas le temps de lire les détails de la méthode, peut supposer que vous ne faites qu’une itération une fois – alors ils vous transmettent un object coûteux. Votre signature de méthode n’indique aucune des deux manières.

En changeant la signature de la méthode en IList / ICollection , vous clarifiez au moins les attentes de l’appelant et évitez les erreurs coûteuses.

Sinon, la plupart des développeurs examinant la méthode peuvent supposer que vous ne faites qu’une itération une fois. Si prendre un IEnumerable est si important, vous devriez envisager de faire le .ToList() au début de la méthode.

C’est dommage que .NET n’ait pas une interface IEnumerable + Count + Indexer, sans méthodes Add / Remove, etc.

Si vos données sont toujours reproductibles, ne vous inquiétez peut-être pas. Cependant, vous pouvez aussi le dérouler – ceci est particulièrement utile si les données entrantes peuvent être volumineuses (par exemple, lecture à partir d’un disque / réseau):

 if(objects == null) throw new ArgumentException(); using(var iter = objects.GetEnumerator()) { if(!iter.MoveNext()) throw new ArgumentException(); var firstObject = iter.Current; var list = DoSomeThing(firstObject); while(iter.MoveNext()) { list.Add(DoSomeThingElse(iter.Current)); } return list; } 

Note J’ai changé un peu la sémantique de DoSomethingElse, mais ceci est principalement pour montrer l’utilisation non déroulée. Vous pouvez par exemple reconditionner l’iterator. Vous pourriez en faire un bloc iterator, ce qui pourrait être sympa; alors il n’y a pas de list – et vous obtiendrez les articles que vous les obtenez au lieu de les append à une liste à retourner.

Si le but est vraiment d’éviter de multiples énumérations que la réponse de Marc Gravell est celle de lire, mais en conservant la même sémantique, vous pouvez simplement supprimer les appels Any et First redondants et aller avec:

 public List Foo(IEnumerable objects) { if (objects == null) throw new ArgumentNullException("objects"); var first = objects.FirstOrDefault(); if (first == null) throw new ArgumentException( "Empty enumerable not supported.", "objects"); var list = DoSomeThing(first); var secondList = DoSomeThingElse(objects); list.AddRange(secondList); return list; } 

Notez que cela suppose que IEnumerable n’est pas générique ou du moins contraint à être un type de référence.

Je surcharge généralement ma méthode avec IEnumerable et IList dans cette situation.

 public static IEnumerable Method( this IList source ){... } public static IEnumerable Method( this IEnumerable source ) { /*input checks on source parameter here*/ return Method( source.ToList() ); } 

Je prends soin d’expliquer dans les commentaires récapitulatifs des méthodes que l’appel à IEnumerable effectuera une .ToList ().

Le programmeur peut choisir d’utiliser .ToList () à un niveau supérieur si plusieurs opérations sont en cours de concaténation, puis appeler la surcharge IList ou laisser ma surcharge IEnumerable s’en occuper.

Utiliser IReadOnlyCollection ou IReadOnlyList dans la signature de la méthode au lieu de IEnumerable a l’avantage de rendre explicite la nécessité de vérifier le compte avant l’itération ou d’itérer plusieurs fois pour une autre raison.

Cependant, ils ont un gros inconvénient qui causera des problèmes si vous essayez de refactoriser votre code pour utiliser des interfaces, par exemple pour le rendre plus testable et convivial pour le proxy dynamic. Le point clé est que IList n’hérite pas de IReadOnlyList , et de même pour les autres collections et leurs interfaces en lecture seule respectives. (En bref, c’est parce que .NET 4.5 voulait conserver la compatibilité d’ABI avec les versions antérieures. Mais ils n’ont même pas profité de l’occasion pour changer cela dans le kernel .NET. )

Cela signifie que si vous obtenez un IList partir d’une partie du programme et que vous voulez le transmettre à une autre partie qui attend un IReadOnlyList , vous ne pouvez pas! Vous pouvez cependant passer un IList tant que IEnumerable .

Au final, IEnumerable est la seule interface en lecture seule prise en charge par toutes les collections .NET, y compris toutes les interfaces de collection. Toute autre alternative reviendra pour vous mordre alors que vous réalisez que vous vous êtes bloqué dans certains choix d’architecture. Donc, je pense que c’est le type approprié à utiliser dans les signatures de fonction pour exprimer que vous voulez juste une collection en lecture seule.

(Notez que vous pouvez toujours écrire une IReadOnlyList ToReadOnly(this IList list) qui est lancée si le type sous-jacent prend en charge les deux interfaces, mais vous devez l’append manuellement IEnumerable est toujours compatible.)

Comme toujours, ce n’est pas un absolu, si vous écrivez un code lourd pour une firebase database où une énumération multiple accidentelle serait un désastre, vous préféreriez peut-être un compromis différent.

Tout d’abord, cet avertissement ne signifie pas toujours autant. Je l’ai habituellement désactivé après m’être assuré que ce n’était pas un goulot performant. Cela signifie simplement que IEnumerable est évalué deux fois, ce qui n’est généralement pas un problème à moins que l’ evaluation elle-même ne prenne beaucoup de temps. Même si cela prend beaucoup de temps, dans ce cas, vous ne pouvez utiliser qu’un élément la première fois.

Dans ce scénario, vous pourriez également exploiter davantage les puissantes méthodes d’extension linq.

 var firstObject = objects.First(); return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList(); 

Il est possible d’évaluer le IEnumerable une seule fois dans ce cas avec un peu de soucis, mais profilez d’abord et voyez si c’est vraiment un problème.