web-dev-qa-db-fra.com

Gestion des avertissements pour une énumération multiple possible de IEnumerable

Dans mon code dans le besoin d'utiliser un IEnumerable<> plusieurs fois donc obtenir l'erreur Resharper de "Énumération multiple possible de IEnumerable".

Exemple de code:

public List<object> Foo(IEnumerable<object> 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 objects pour qu'il soit List et ensuite éviter la possibilité d'énumération multiple, mais je ne reçois pas l'objet le plus élevé que je puisse gérer. 
  • Une autre chose que je peux faire est de convertir la IEnumerable en List au début de la méthode: 

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

Mais ceci est juste maladroit .

Que feriez-vous dans ce scénario?

318
gdoron

Le problème avec IEnumerable en tant que paramètre est qu’il indique aux appelants "Je souhaite énumérer cette". Cela ne leur dit pas combien de fois vous souhaitez énumérer. 

Je peux changer le paramètre de l'objet en Liste et ensuite éviter la possibilité d'énumération multiple, mais je ne reçois pas d'objet l'objet le plus élevé que je puisse gérer .

L'objectif de prendre l'objet le plus élevé est noble, mais il laisse trop de place à des hypothèses. Voulez-vous vraiment que quelqu'un transmette une requête LINQ to SQL à cette méthode, uniquement pour l'énumérer deux fois (en obtenant 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 répétez l’itération qu’une fois - pour vous transmettre un objet coûteux. La signature de votre méthode n'indique ni l'un ni l'autre. 

En modifiant la signature de la méthode en IList/ICollection, vous expliquez au moins plus clairement à vos interlocuteurs quelles sont vos attentes et vous éviterez ainsi des erreurs coûteuses. 

Autrement, la plupart des développeurs qui consultent cette méthode peuvent supposer que vous n’effectuez une itération qu’une fois. Si il est si important de prendre IEnumerable, envisagez de faire la fonction .ToList() au début de la méthode. 

C’est dommage. NET n’a pas d’interface qui soit IEnumerable + Count + Indexer, sans les méthodes Add/Remove etc., ce qui, je suppose, résoudrait ce problème. 

426
Paul Stovell

Si vos données doivent toujours être reproductibles, ne vous en inquiétez peut-être pas. Toutefois, vous pouvez également le dérouler - ceci est particulièrement utile si les données entrantes peuvent être volumineuses (par exemple, en lisant à 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;
}

Remarque J'ai un peu modifié la sémantique de DoSomethingElse, mais c'est principalement pour montrer l'utilisation non déroulée. Vous pouvez ré-emballer l'itérateur, par exemple. Vous pourriez aussi en faire un bloc d'itérateurs, ce qui pourrait être Nice; alors il n'y a pas list - et vous yield returnz les articles au fur et à mesure que vous les obtenez, plutôt que de les ajouter à une liste à restituer.

28
Marc Gravell

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

public List<object> Foo(IEnumerable<object> 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 vous IEnumerable n'est pas générique ou du moins qu'il est contraint d'être un type de référence.

4
João Angelo

Je surcharge habituellement ma méthode avec IEnumerable et IList dans cette situation. 

public static IEnumerable<T> Method<T>( this IList<T> source ){... }

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

Je me charge d’expliquer dans les commentaires résumés des méthodes selon lesquelles l’appel de IEnumerable effectuera un .ToList ().

Le programmeur peut choisir .ToList () à un niveau supérieur si plusieurs opérations sont concaténées, puis appeler la surcharge IList ou laisser ma surcharge IEnumerable s'en charger.

4
Mauro Sampietro

L'utilisation de IReadOnlyCollection<T> ou IReadOnlyList<T> dans la signature de la méthode au lieu de IEnumerable<T> présente l'avantage de préciser qu'il peut être nécessaire de vérifier le nombre avant itération ou d'itérer plusieurs fois pour une autre raison.

Cependant, ils ont un énorme inconvénient qui posera des problèmes si vous essayez de refactoriser votre code pour utiliser des interfaces, par exemple pour le rendre plus testable et plus convivial pour le proxy dynamique. Le point clé est que IList<T> N'H&EACUTE;RITE PAS DE IReadOnlyList<T>, ni de la même manière pour les autres collections et leurs interfaces en lecture seule respectives. (En bref, cela est dû au fait que .NET 4.5 souhaitait conserver la compatibilité ABI avec les versions précédentes. Mais ils n’en ont même pas profité pour changer cela dans le noyau .NET. )

Cela signifie que si vous obtenez un IList<T> d'une partie du programme et souhaitez le transmettre à une autre partie qui attend un IReadOnlyList<T>, vous ne pouvez pas! Vous pouvez cependant passer un IList<T> en tant que IEnumerable<T>.

Finalement, IEnumerable<T> 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 vous mordre au fur et à mesure que vous réaliserez que vous vous êtes exclu de certains choix d'architecture. Je pense donc que c'est le type approprié à utiliser dans les signatures de fonction pour exprimer le fait que vous voulez juste une collection en lecture seule.

(Notez que vous pouvez toujours écrire une méthode d'extension IReadOnlyList<T> ToReadOnly<T>(this IList<T> list) qui effectue des conversions simples si le type sous-jacent prend en charge les deux interfaces, mais vous devez l'ajouter manuellement partout lors du refactoring, où as IEnumerable<T> est toujours compatible.)

Comme toujours, il ne s'agit pas d'un absolu. Si vous écrivez un code lourd en base de données dans lequel une énumération multiple accidentelle serait un désastre, vous préférerez peut-être un compromis différent.

3
Gabriel Morin

Si vous ne devez vérifier que le premier élément, vous pouvez y jeter un coup d'œil sans parcourir la collection entière

public List<object> Foo(IEnumerable<object> objects)
{
    object firstObject;
    if (objects == null || !TryPeek(ref objects, out firstObject))
        throw new ArgumentException();

    var list = DoSomeThing(firstObject);
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}

public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    IEnumerator<T> enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        first = default(T);
        source = Enumerable.Empty<T>();
        return false;
    }

    first = enumerator.Current;
    T firstElement = first;
    source = Iterate();
    return true;

    IEnumerable<T> Iterate()
    {
        yield return firstElement;
        using (enumerator)
        {
            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
            }
        }
    }
}
0
Madruel