2010-01-13 7 views
267

Ich habe eine IEnumerable<T> Methode, die ich verwende, um Steuerelemente in einer WebForms-Seite zu finden.IEnumerable und Rekursion mit Renditerückgabe

Die Methode ist rekursiv und ich habe einige Probleme beim Zurückgeben der Art, die ich will, wenn die yield return den Wert des rekursiven Aufrufs zurückgibt.

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

Das zur Zeit wirft einen Fehler „Kann nicht Ausdruck Typ umwandeln“. Wenn diese Methode jedoch den Typ IEnumerable<Object> zurückgibt, wird der Code erstellt, aber der falsche Typ wird in der Ausgabe zurückgegeben.

Gibt es eine Möglichkeit, yield return zu verwenden, während auch Rekursion verwendet wird?

+1

http://stackoverflow.com/questions/1815497/enumerating-collections-that-are-not-inherently-ienumerable/ 1815600 # 1815600: Link zu "mrydengrens" Antwort auf den Thread "Auflistungen auflisten, die nicht von Natur aus IEnumerable sind?" Sein Beispielcode basiert auf einem Blog-Artikel von Eric Lippert, der Ihnen zeigt, wie Sie Stacks in rekursiver Enumeration mit Linq verwenden, um so eine möglicherweise teure Speicherbenutzung durch Iteratoren zu vermeiden. Imho sehr nützlich! – BillW

+0

BTW. 'if (c.Controls.Count> 0)' -> 'if (c.Controls.Any())', vor allem, wenn Sie auch nachgeben :) – mayu

+0

Ich glaube nicht, dass dieser Fall von Nachgeben profitiert. Der Vollständigkeit halber habe ich eine Implementierung ohne "Ausbeute" angegeben. Bitte siehe unten :) Und es ist ein One-Liner zu :) – mayu

Antwort

443

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

ersetzen

yield return c.GetDeepControlsByType<T>(); 

mit:

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

Sie benötigen jedes der Elemente erhalten durch den rekursiven Aufruf ergab:

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

Hinweis, dass es eine Kosten rekursive auf diese Weise - Sie werden am Ende eine Menge von iter erstellen Sie können ein Leistungsproblem verursachen, wenn Sie einen wirklich tiefen Kontrollbaum haben. Wenn Sie das vermeiden möchten, müssen Sie die Rekursion im Wesentlichen selbst innerhalb der Methode durchführen, um sicherzustellen, dass nur ein Iterator (Zustandsautomat) erstellt wird. Weitere Details und eine Beispielimplementierung finden Sie unter this question - aber das bringt natürlich auch eine gewisse Komplexität mit sich.

+2

Ich finde es überraschend, dass in einem Thread über die Erfüllung Jon nicht erwähnt hat "c.Controls.Count> 0" vs. ".Any()' :) – mayu

+0

@Tymek tatsächlich ist es in der verknüpften Antwort erwähnt. –

12

Sie müssen die Artikel vom Enumerator zurück, nicht den Enumerator 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; 
      } 
     } 
    } 
} 
9

Ich glaube, Sie Rückkehr jeder der Kontrollen in den enumerables ergeben haben.

13

Andere gaben Ihnen die richtige Antwort, aber ich denke nicht, dass Ihr Fall von Nachgeben profitiert.

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

Verwendet LINQ 'yield' auch nicht? ;) –

+0

Das ist glatt. Ich war schon immer von der zusätzlichen 'foreach'-Schleife gestört. Jetzt kann ich das mit reiner funktionaler Programmierung machen! – jsuddsjr

+1

Ich mag diese Lösung in Bezug auf die Lesbarkeit, aber es ist das gleiche Leistungsproblem mit Iteratoren wie die Verwendung von Ertrag. @PhilippM: Bestätigt, dass LINQ verwendet Ertrag https://referencesource.microsoft.com/System.Core/R/577032c8811e20d3.html – Herman

6

Seredynski's syntax ist richtig, aber Sie sollten vorsichtig sein, yield return in rekursiven Funktionen zu vermeiden, weil es eine Katastrophe für die Speichernutzung ist. Siehe https://stackoverflow.com/a/3970171/284795 es skaliert explosionsartig mit der Tiefe (eine ähnliche Funktion verwendet 10% des Speichers in meiner App).

Eine einfache Lösung ist eine Liste verwenden und es mit der Rekursion passieren https://codereview.stackexchange.com/a/5651/754

/// <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

15

Als Jon Skeet und Oberst zu beseitigen Paniknotizen in ihren Antworten, die in rekursiven Methoden yield return verwenden, können zu Leistungsproblemen führen, wenn der Baum sehr tief ist.

Hier ist eine generische nicht-rekursive Erweiterung Methode, die einen depth-first Traversal einer Folge von Bäumen fü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 zu Eric Lippert's solution arbeitet RecursiveSelect direkt mit Aufzählungen, so dass es nicht braucht Call Reverse (die die gesamte Sequenz im Speicher zwischenspeichert).

Mit RecursiveSelect, dem ursprünglichen Methode des OP kann einfach so umgeschrieben werden:

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

Damit dieser (ausgezeichnete) Code funktioniert, musste ich 'OfType' verwenden, um die ControlCollection in das IEnumerable-Format zu bringen. In Windows Forms ist eine ControlCollection nicht aufzählbar: return control.Controls.OfType () .RecursiveSelect (c => c.Controls.OfType ()) . Wo (c => c ist T); – BillW

0

Zwar gibt es viele gute Antworten gibt, würde ich noch hinzufügen, dass es möglich ist, LINQ Methoden zu verwenden, um das gleiche zu erreichen Ding, .

Zum Beispiel könnte der ursprüngliche Code des OP neu geschrieben werden als:

public static IEnumerable<Control> 
          GetDeepControlsByType<T>(this Control control) 
{ 
    return control.Controls.OfType<T>() 
      .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));   
} 
+0

Eine Lösung mit diesem Ansatz wurde vor drei Jahren * veröffentlicht. – Servy

+0

@Servy Obwohl es ähnlich ist (was BTW ich verpasste zwischen allen Antworten ... beim Schreiben dieser Antwort), ist es immer noch anders, wie es .OfType <> verwendet, um zu filtern, und .Union() –

+2

Die 'OfType' ist nicht wirklich anders. Höchstens eine geringfügige styalistische Veränderung.Ein Steuerelement kann kein untergeordnetes Element mehrerer Steuerelemente sein, daher ist der durchquerte Baum * bereits * unqiue. Die Verwendung von "Union" anstelle von "Concat" verifiziert unnötig die Einzigartigkeit einer Sequenz, die bereits garantiert einzigartig ist, und ist daher ein objektives Downgrade. – Servy