it-swarm.com.de

Warnung bezüglich möglicher mehrfacher Aufzählung von IEnumerable behandeln

In meinem Code muss ein IEnumerable<> mehrmals verwendet werden, um den Resharper-Fehler "Mögliche mehrfache Aufzählung von IEnumerable" zu erhalten.

Beispielcode:

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;
}
  • Ich kann den Parameter objects so ändern, dass er List ist, und dann die mögliche Mehrfachaufzählung vermeiden, aber ich bekomme nicht das höchste Objekt, das ich verarbeiten kann. 
  • Eine andere Sache, die ich tun kann, ist die IEnumerable in List am Anfang der Methode zu konvertieren: 

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

Aber das ist nur umständlich .

Was würdest du in diesem Szenario tun?

318
gdoron

Das Problem bei der Verwendung von IEnumerable als Parameter besteht darin, dass Anrufer "Ich möchte das aufzählen" sagen. Es sagt ihnen nicht, wie oft Sie aufzählen wollen. 

Ich kann den object-Parameter in List ändern und dann die mögliche Mehrfachaufzählung vermeiden, aber dann erhalte ich nicht das höchste Objekt, das ich behandeln kann.

Das Ziel, das höchste Objekt zu nehmen, ist edel, lässt aber zu viele Annahmen zu. Möchten Sie wirklich, dass eine LINQ-an-SQL-Abfrage an diese Methode übergeben wird, nur um sie zweimal aufzuzählen (und jedes Mal potenziell unterschiedliche Ergebnisse erhalten?)

Die Semantik, die hier fehlt, ist, dass ein Anrufer, der sich möglicherweise nicht die Zeit nimmt, um die Details der Methode zu lesen, möglicherweise nur einmal iterieren muss - und daher ein teures Objekt übergeben wird. Ihre Methodensignatur gibt keinen Weg an. 

Durch Ändern der Methodensignatur in IList/ICollection machen Sie dem Anrufer zumindest klar, welche Erwartungen Sie haben, und sie können kostspielige Fehler vermeiden. 

Andernfalls können die meisten Entwickler, die die Methode betrachten, annehmen, dass Sie nur einmal iterieren. Wenn die IEnumerable so wichtig ist, sollten Sie die .ToList() zu Beginn der Methode in Betracht ziehen. 

Es ist eine Schande. .NET hat keine IEnumerable + Count + Indexer-Schnittstelle ohne Add/Remove-Methoden. Dies ist, was ich vermute, würde dieses Problem lösen. 

426
Paul Stovell

Wenn Ihre Daten immer reproduzierbar sind, machen Sie sich vielleicht keine Sorgen. Sie können es jedoch auch abrollen. Dies ist besonders nützlich, wenn die eingehenden Daten groß sein könnten (z. B. Lesen von Datenträger/Netzwerk):

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;
}

Hinweis Ich habe die Semantik von DoSomethingElse ein wenig geändert, aber dies soll hauptsächlich die entrollte Verwendung zeigen. Sie könnten beispielsweise den Iterator neu einwickeln. Man könnte es auch zu einem Iteratorblock machen, der Nizza sein könnte; dann gibt es keine list - und Sie würden die Elemente, sobald Sie sie erhalten, yield returnen, anstatt sie einer Liste hinzuzufügen, die zurückgegeben werden soll.

28
Marc Gravell

Wenn wirklich das Ziel besteht, mehrere Aufzählungen zu verhindern, dann ist die Antwort von Marc Gravell zu lesen, aber unter Beibehaltung der gleichen Semantik können Sie einfach die redundanten Any- und First-Aufrufe entfernen und gehen mit:

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;
}

Beachten Sie, dass dies davon ausgeht, dass Sie IEnumerable nicht generisch ist oder zumindest als Referenztyp festgelegt ist.

4
João Angelo

Ich überlade meine Methode in dieser Situation normalerweise mit IEnumerable und IList. 

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() );
}

Ich erkläre in der Zusammenfassung der Methoden, dass der Aufruf von IEnumerable eine .ToList () ausführt.

Der Programmierer kann .ToList () auf einer höheren Ebene wählen, wenn mehrere Vorgänge verkettet werden, und dann die IList-Überladung aufrufen oder sich meine IEnumerable-Überladung darum kümmern.

4
Mauro Sampietro

Die Verwendung von IReadOnlyCollection<T> oder IReadOnlyList<T> in der Methodensignatur anstelle von IEnumerable<T> hat den Vorteil, dass Sie explizit angeben müssen, dass Sie die Anzahl vor dem Iterieren überprüfen müssen oder aus einem anderen Grund mehrmals iterieren.

Sie haben jedoch einen großen Nachteil, der Probleme verursachen kann, wenn Sie versuchen, Ihren Code für die Verwendung von Schnittstellen zu refaktorisieren, z. B. um ihn testfähiger und für dynamisches Proxying benutzerfreundlicher zu machen. Der Schlüsselpunkt ist, dass IList<T> NICHT VON IReadOnlyList<T> erbt, und in ähnlicher Weise für andere Sammlungen und ihre jeweiligen schreibgeschützten Schnittstellen. (Kurz gesagt, dies liegt daran, dass .NET 4.5 die ABI-Kompatibilität mit früheren Versionen beibehalten wollte. Aber sie haben nicht einmal die Gelegenheit genutzt, dies im .NET-Core zu ändern. )

Das heißt, wenn Sie einen IList<T> von einem Teil des Programms erhalten und ihn an einen anderen Teil übergeben möchten, der einen IReadOnlyList<T> erwartet, können Sie das nicht! Sie können jedoch einen IList<T> als IEnumerable<T> übergeben.

Am Ende ist IEnumerable<T> die einzige schreibgeschützte Schnittstelle, die von allen .NET-Auflistungen einschließlich aller Auflistungsschnittstellen unterstützt wird. Jede andere Alternative wird zurückkehren, um Sie zu beißen, wenn Sie feststellen, dass Sie sich von einigen Architekturoptionen ausgeschlossen haben. Ich denke, es ist der richtige Typ, der in Funktionssignaturen verwendet wird, um auszudrücken, dass Sie nur eine schreibgeschützte Sammlung wünschen.

(Beachten Sie, dass Sie immer eine Erweiterungsmethode IReadOnlyList<T> ToReadOnly<T>(this IList<T> list) schreiben können, die einfache Umsetzungen vornimmt, wenn der zugrunde liegende Typ beide Schnittstellen unterstützt. Sie müssen sie jedoch beim Refactoring überall hinzufügen, wobei IEnumerable<T> immer kompatibel ist.)

Wie immer ist dies kein absolutes Muss. Wenn Sie datenbankintensiven Code schreiben, bei dem eine versehentliche mehrfache Aufzählung eine Katastrophe darstellt, können Sie einen anderen Kompromiss bevorzugen.

3
Gabriel Morin

Wenn Sie nur das erste Element überprüfen müssen, können Sie einen Blick darauf werfen, ohne die gesamte Sammlung zu durchlaufen:

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