2010-01-13 4 views
267

J'ai une méthode IEnumerable<T> que j'utilise pour rechercher des contrôles dans une page WebForms.IEnumerable et récursivité à l'aide de rendement return

La méthode est récursive et j'ai quelques problèmes pour renvoyer le type que je veux quand le yield return renverra la valeur de l'appel récursif.

Mon code se présente comme suit:

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

Cela jette actuellement une erreur "Impossible de convertir le type d'expression". Si toutefois cette méthode renvoie le type IEnumerable<Object>, le code est généré, mais le type incorrect est renvoyé dans la sortie.

Existe-t-il un moyen d'utiliser yield return tout en utilisant la récursivité?

+1

http://stackoverflow.com/questions/1815497/enumerating-collections-that-are-not-inherently-ienumerable/ 1815600 # 1815600: lien vers "mrydengrens" réponse sur le sujet "énumération des collections qui ne sont pas intrinsèquement IEnumerable?" Son exemple de code est basé sur un article de blog d'Eric Lippert vous montrant comment utiliser les piles dans l'énumération récursive avec Linq, évitant ainsi l'utilisation de la mémoire par les itérateurs. imho très utile! – BillW

+0

BTW. 'if (c.Controls.Count> 0)' -> 'if (c.Controls.Any())', surtout si vous cédez aussi :) – mayu

+0

Je ne pense pas que cette affaire puisse être rentable. Pour être complet, j'ai fourni une implémentation sans "yield". S'il vous plaît voir ci-dessous :) Et c'est un one-liner aussi :) – mayu

Répondre

443

l'intérieur d'une méthode qui retourne IEnumerable<T>, yield return doit retourner T, pas IEnumerable<T>.

Remplacer

yield return c.GetDeepControlsByType<T>(); 

avec:

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

Vous devez obtenir chacun des éléments produites par l'appel récursif:

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

Notez qu'il ya un coût pour récursif de cette façon - vous finirez par créer beaucoup d'iter ators, ce qui peut créer un problème de performance si vous avez un arbre de contrôle vraiment profond. Si vous voulez éviter cela, vous devez essentiellement faire la récursion vous-même dans la méthode, pour vous assurer qu'il n'y a qu'un seul itérateur (machine d'état) créé. Voir this question pour plus de détails et un exemple d'implémentation - mais cela ajoute évidemment une certaine quantité de complexité.

+2

Je trouve surprenant que dans un thread sur le rendement de Jon n'a pas mentionné 'c.Controls.Count> 0' vs' .Any() ':) – mayu

+0

@Tymek en fait, il est mentionné dans la réponse liée. –

12

Vous devez retourner les articles du recenseur, pas le recenseur lui-même, dans votre deuxième 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

Je pense que vous devez obtenir le retour de chacun des contrôles dans les enumerables.

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

D'autres vous ont fourni la bonne réponse, mais je ne pense pas que votre cas soit intéressant.

Voici un extrait qui permet d'obtenir le même résultat sans céder.

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

N'utilise pas bien LINQ 'yield'? ;) –

+0

Ceci est lisse. J'ai toujours été dérangé par la boucle supplémentaire foreach. Maintenant, je peux le faire avec une programmation fonctionnelle pure! – jsuddsjr

+1

J'aime cette solution en termes de lisibilité, mais elle rencontre le même problème de performance avec les itérateurs qu'avec le rendement. @PhilippM: Vérifié que LINQ utilise le rendement https://referencesource.microsoft.com/System.Core/R/577032c8811e20d3.html – Herman

6

Seredynski's syntax est correct, mais vous devez être prudent pour éviter yield return dans les fonctions récursives, car il est un désastre pour l'utilisation de la mémoire. Voir https://stackoverflow.com/a/3970171/284795 il échelles explosive avec la profondeur (une fonction similaire utilisait 10% de la mémoire dans mon application).

Une solution simple est d'utiliser une liste et le transmettre avec la récursion 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); 
    } 
} 

Sinon, vous pouvez utiliser une pile et une boucle while pour éliminer les appels récursifs https://codereview.stackexchange.com/a/5661/754

15

Comme Jon Skeet et le colonel La note de panique dans leurs réponses, en utilisant yield return dans les méthodes récursives peut causer des problèmes de performance si l'arbre est très profond.

est ici une méthode non-récursive générique extension qui effectue un parcours en profondeur d'abord d'une séquence d'arbres:

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

Contrairement Eric Lippert's solution, RecursiveSelect travaille directement avec les agents recenseurs afin qu'il n'a pas besoin de appel inverse (qui tamponne la séquence entière dans la mémoire).

En utilisant RecursiveSelect, la méthode originale OP peut être simplement réécrite comme ceci:

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

Pour que ce code (excellent) fonctionne, j'ai dû utiliser OfType pour obtenir le ControlCollection dans le formulaire IEnumerable; dans Windows Forms, une ControlCollection n'est pas énumérable: return control.Controls.OfType () .RecursiveSelect (c => c.Controls.OfType ()) .Où (c => c est T); – BillW

0

Bien qu'il existe de nombreuses bonnes réponses là-bas, je voudrais encore ajouter qu'il est possible d'utiliser des méthodes LINQ pour accomplir la même chose,

Par exemple, le code d'origine de l'OP pourrait être réécrite comme:

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

Une solution utilisant cette même approche a été publiée * il y a trois ans *. – Servy

+0

@Servy Bien qu'il soit similaire (ce qui BTW j'ai manqué entre toutes les réponses ... tout en écrivant cette réponse), il est encore différent, car il utilise .OfType <> pour filtrer, et .Union() –

+2

Le 'OfType' n'est pas vraiment différent. Tout au plus un changement styaliste mineur.Un contrôle ne peut pas être un enfant de plusieurs contrôles, donc l'arbre traversé est * déjà * unqiue. Utiliser 'Union' au lieu de' Concat' vérifie inutilement l'unicité d'une séquence déjà garantie d'être unique, et est donc une rétrogradation objective. – Servy

Questions connexes