348 Stimmen

IEnumerable und Rekursion mit yield return

Ich habe eine IEnumerable<T> Methode, die ich verwende, um Steuerelemente in einer WebForms-Seite zu finden.

Die Methode ist rekursiv und ich habe einige Probleme mit der Rückgabe des Typs, den ich möchte, wenn die yield return gibt den Wert des rekursiven Aufrufs zurück.

Mein Code sieht wie folgt aus:

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    foreach(Control c in control.Controls)
    {
        if (c is T)
        {
            yield return c;
        }

        if(c.Controls.Count > 0)
        {
            yield return c.GetDeepControlsByType<T>();
        }
    }
}

Dies führt derzeit zu einem "Cannot convert expression type"-Fehler. Wenn diese Methode jedoch den Typ IEnumerable<Object> wird der Code erstellt, aber in der Ausgabe wird der falsche Typ zurückgegeben.

Gibt es eine Möglichkeit zur Verwendung von yield return und gleichzeitig eine Rekursion verwenden?

521voto

Marcin Seredynski Punkte 6907

Innerhalb einer Methode, die Folgendes zurückgibt IEnumerable<T> , yield return muss zurückkehren T , nicht ein IEnumerable<T> .

Ersetzen Sie

yield return c.GetDeepControlsByType<T>();

mit:

foreach (var x in c.GetDeepControlsByType<T>())
{
  yield return x;
}

121voto

Jon Skeet Punkte 1325502

Sie müssen nachgeben jedes der Elemente die der rekursive Aufruf liefert:

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    foreach(Control c in control.Controls)
    {
        if (c is T)
        {
            yield return c;
        }

        if(c.Controls.Count > 0)
        {
            foreach (Control control in c.GetDeepControlsByType<T>())
            {
                yield return control;
            }
        }
    }
}

Beachten Sie, dass die Rekursion auf diese Weise ihren Preis hat - Sie werden am Ende eine Menge Iteratoren erstellen, was zu Leistungsproblemen führen kann, wenn Sie einen sehr tiefen Kontrollbaum haben. Wenn Sie das vermeiden wollen, müssen Sie die Rekursion innerhalb der Methode selbst durchführen, um sicherzustellen, dass nur ein Iterator (Zustandsautomat) erstellt wird. Siehe diese Frage für weitere Details und eine Beispiel-Implementierung - aber das bringt natürlich auch ein gewisses Maß an Komplexität mit sich.

51voto

Michael Liu Punkte 48128

Wie Jon Skeet und Colonel Panic in ihren Antworten feststellen, ist die Verwendung von yield return in rekursiven Methoden kann zu Leistungsproblemen führen, wenn der Baum sehr tief ist.

Hier ist ein allgemeines nicht rekursiv Erweiterungsmethode, die eine Durchquerung einer Folge von Bäumen in der Tiefe vornimmt:

public static IEnumerable<TSource> RecursiveSelect<TSource>(
    this IEnumerable<TSource> source, Func<TSource, IEnumerable<TSource>> childSelector)
{
    var stack = new Stack<IEnumerator<TSource>>();
    var enumerator = source.GetEnumerator();

    try
    {
        while (true)
        {
            if (enumerator.MoveNext())
            {
                TSource element = enumerator.Current;
                yield return element;

                stack.Push(enumerator);
                enumerator = childSelector(element).GetEnumerator();
            }
            else if (stack.Count > 0)
            {
                enumerator.Dispose();
                enumerator = stack.Pop();
            }
            else
            {
                yield break;
            }
        }
    }
    finally
    {
        enumerator.Dispose();

        while (stack.Count > 0) // Clean up in case of an exception.
        {
            enumerator = stack.Pop();
            enumerator.Dispose();
        }
    }
}

Anders als Die Lösung von Eric Lippert RecursiveSelect arbeitet direkt mit Aufzählungszeichen, so dass es nicht Reverse aufrufen muss (das die gesamte Sequenz im Speicher puffert).

Mit RecursiveSelect kann die ursprüngliche Methode des Autors einfach wie folgt umgeschrieben werden:

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T);
}

21voto

tymtam Punkte 23765

Andere haben Ihnen die richtige Antwort gegeben, aber ich glaube nicht, dass es in Ihrem Fall von Vorteil ist, nachzugeben.

Hier ist ein Schnipsel, der das Gleiche erreicht, ohne nachzugeben.

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
   return control.Controls
                 .Where(c => c is T)
                 .Concat(control.Controls
                                .SelectMany(c =>c.GetDeepControlsByType<T>()));
}

15voto

Rob Levine Punkte 38688

Sie müssen die Artikel von der Aufzählungsliste, nicht von der Aufzählungsliste selbst, in Ihrem zweiten yield return

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    foreach (Control c in control.Controls)
    {
        if (c is T)
        {
            yield return c;
        }

        if (c.Controls.Count > 0)
        {
            foreach (Control ctrl in c.GetDeepControlsByType<T>())
            {
                yield return ctrl;
            }
        }
    }
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X