IEnumerable und Rekursion mit Ertragsrendite

307

Ich habe eine IEnumerable<T>Methode, mit der ich Steuerelemente auf einer WebForms-Seite finde.

Die Methode ist rekursiv und ich habe einige Probleme, den gewünschten Typ zurückzugeben, wenn yield returnder Wert des rekursiven Aufrufs zurückgegeben wird.

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 löst derzeit den Fehler "Ausdruckstyp kann nicht konvertiert werden" aus. Wenn diese Methode IEnumerable<Object>jedoch den Typ zurückgibt , wird der Code erstellt, aber der falsche Typ wird in der Ausgabe zurückgegeben.

Gibt es eine Möglichkeit yield return, Rekursion zu verwenden?

Jamie Dixon
quelle
1
stackoverflow.com/questions/1815497/… : Link zur Antwort "mrydengrens" im Thread "Auflisten von Sammlungen, die nicht von Natur aus IEnumerable sind?" Sein Beispielcode basiert auf einem Blog-Artikel von Eric Lippert, der Ihnen zeigt, wie Sie Stapel in rekursiver Aufzählung mit Linq verwenden, um möglicherweise eine teure Speichernutzung durch Iteratoren zu vermeiden. imho sehr nützlich!
BillW
Übrigens. if(c.Controls.Count > 0)-> if(c.Controls.Any()), besonders wenn Sie auch nachgeben :)
Tymtam
Ich glaube nicht, dass dieser Fall vom Nachgeben profitiert. Der Vollständigkeit halber habe ich eine Implementierung ohne bereitgestellt yield. Bitte siehe unten :) Und es ist auch ein
Einzeiler
Sie sollten darauf achten, yield returnbei rekursiven Funktionen zu vermeiden , dass die Speichernutzung explosionsartig skaliert. Siehe stackoverflow.com/a/30300257/284795
Colonel Panic

Antworten:

485

Innerhalb einer Methode, die zurückgibt IEnumerable<T>, yield returnmuss zurückgegeben werden T, nicht eine IEnumerable<T>.

Ersetzen

yield return c.GetDeepControlsByType<T>();

mit:

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

Sie müssen jedes der Elemente zurückgeben, die durch den rekursiven Aufruf erhalten werden:

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 das Rekursieren auf diese Weise mit Kosten verbunden ist. Am Ende werden viele Iteratoren erstellt, die zu Leistungsproblemen führen können, wenn Sie über einen wirklich tiefen Kontrollbaum verfügen. Wenn Sie dies vermeiden möchten, müssen Sie die Rekursion innerhalb der Methode grundsätzlich selbst durchführen, um sicherzustellen, dass nur ein Iterator (Zustandsmaschine) erstellt wird. In dieser Frage finden Sie weitere Details und eine Beispielimplementierung. Dies führt jedoch natürlich auch zu einer gewissen Komplexität.

Jon Skeet
quelle
2
Ich finde es überraschend, dass in einem Thread über das Nachgeben Jon nicht erwähnt hat c.Controls.Count > 0vs. .Any():)
Tymtam
@Tymek eigentlich ist es in der verknüpften Antwort erwähnt.
28

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

Hier ist eine generische nicht rekursive Erweiterungsmethode, die eine Tiefenüberquerung einer Folge von Bäumen durchführt:

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

Im Gegensatz zur Lösung von Eric Lippert arbeitet RecursiveSelect direkt mit Enumeratoren, sodass Reverse nicht aufgerufen werden muss (wodurch die gesamte Sequenz im Speicher gepuffert wird).

Mit RecursiveSelect kann die ursprüngliche Methode des OP 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);
}
Michael Liu
quelle
Damit dieser (ausgezeichnete) Code funktioniert, musste ich 'OfType' verwenden, um die ControlCollection in IEnumerable-Form zu bringen. In Windows Forms ist eine ControlCollection nicht aufzählbar: return control.Controls.OfType <Control> () .RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()) .Where (c => c ist T. );
BillW
17

Andere haben Ihnen die richtige Antwort gegeben, aber ich glaube nicht, dass Ihr Fall vom Nachgeben profitiert.

Hier ist ein Ausschnitt, der dasselbe 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>()));
}
Tymtam
quelle
2
Verwendet LINQ nicht yieldauch? ;)
Philipp M
Das ist schlau. Die zusätzliche foreachSchleife hat mich immer gestört . Jetzt kann ich das mit rein funktionaler Programmierung machen!
jsuddsjr
1
Ich mag diese Lösung in Bezug auf die Lesbarkeit, aber sie hat mit Iteratoren das gleiche Leistungsproblem wie die Verwendung von Yield. @PhilippM: Verifiziert, dass LINQ Yield Referencesource verwendet.microsoft.com/System.Core/R/…
Herman
Daumen hoch für eine großartige Lösung.
Tomer W
12

Sie müssen die Elemente in Ihrem zweiten vom Enumerator zurückgeben, nicht vom Enumerator selbstyield 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;
            }
        }
    }
}
Rob Levine
quelle
9

Ich denke, Sie müssen jedes der Steuerelemente in den Aufzählungszeichen zurückgeben.

    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 childControl in c.GetDeepControlsByType<T>())
                {
                    yield return childControl;
                }
            }
        }
    }
Torbjörn Hansson
quelle
8

Die Syntax von Seredynski ist korrekt, aber Sie sollten darauf achten, yield returnrekursive Funktionen zu vermeiden, da dies eine Katastrophe für die Speichernutzung darstellt. Siehe https://stackoverflow.com/a/3970171/284795. Es skaliert explosionsartig mit der Tiefe (eine ähnliche Funktion verbrauchte 10% des Speichers in meiner App).

Eine einfache Lösung besteht darin, eine Liste zu verwenden und sie mit der Rekursion https://codereview.stackexchange.com/a/5651/754 zu übergeben

/// <summary>
/// Append the descendents of tree to the given list.
/// </summary>
private void AppendDescendents(Tree tree, List<Tree> descendents)
{
    foreach (var child in tree.Children)
    {
        descendents.Add(child);
        AppendDescendents(child, descendents);
    }
}

Alternativ können Sie einen Stapel und eine while-Schleife verwenden, um rekursive Aufrufe https://codereview.stackexchange.com/a/5661/754 zu vermeiden

Oberst Panik
quelle
0

Obwohl es viele gute Antworten gibt, möchte ich dennoch hinzufügen, dass es möglich ist, LINQ-Methoden zu verwenden, um dasselbe zu erreichen.

Zum Beispiel könnte der ursprüngliche Code des OP wie folgt umgeschrieben werden:

public static IEnumerable<Control> 
                           GetDeepControlsByType<T>(this Control control)
{
   return control.Controls.OfType<T>()
          .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));        
}
yoel halb
quelle
Eine Lösung mit demselben Ansatz wurde vor drei Jahren veröffentlicht .
Servy
@Servy Obwohl es ähnlich ist (was ich übrigens zwischen allen Antworten verpasst habe ... beim Schreiben dieser Antwort), ist es immer noch anders, da es .OfType <> zum Filtern und .Union ()
yoel halb
2
Das OfTypeist nicht wirklich ein meainingful anders. Höchstens eine geringfügige styalistische Veränderung. Ein Steuerelement kann nicht mehreren Steuerelementen untergeordnet sein , daher ist der durchquerte Baum bereits eindeutig. Die Verwendung von Unionanstelle von Concatist unnötig, um die Eindeutigkeit einer Sequenz zu überprüfen, die bereits als eindeutig garantiert ist, und ist daher eine objektive Herabstufung.
Servy