IEnumerable et récursivité utilisant return return

307

J'ai une IEnumerable<T>méthode que j'utilise pour trouver des contrôles dans une page WebForms.

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

Mon code ressemble à ceci:

    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 génère actuellement une erreur «Impossible de convertir le type d'expression». Si toutefois cette méthode retourne le type IEnumerable<Object>, le code se construit, mais le mauvais type est renvoyé dans la sortie.

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

Jamie Dixon
la source
1
stackoverflow.com/questions/1815497/… : lien vers la réponse "mrydengrens" sur le fil de discussion "É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 des piles dans l'énumération récursive avec Linq, évitant ainsi une utilisation de mémoire éventuellement coûteuse par les itérateurs. à mon humble avis très utile!
BillW
BTW. if(c.Controls.Count > 0)-> if(c.Controls.Any()), surtout si vous
cédez
Je ne pense pas que cette affaire profite du rendement. Pour être complet, j'ai fourni une implémentation sans yield. Veuillez voir ci-dessous :) Et c'est aussi un
aller simple
Vous devez faire attention à éviter yield returndans les fonctions récursives, l'utilisation de la mémoire évolue de manière explosive. Voir stackoverflow.com/a/30300257/284795
Colonel Panic

Réponses:

485

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

Remplacer

yield return c.GetDeepControlsByType<T>();

avec:

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

Vous devez fournir chacun des éléments générés 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 y a un coût à récurser de cette façon - vous finirez par créer beaucoup d'itérateurs, ce qui peut créer un problème de performances si vous avez une arborescence de contrôle très approfondie. Si vous voulez éviter cela, vous devez essentiellement effectuer la récursivité 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 cette question pour plus de détails et un exemple d'implémentation - mais cela ajoute évidemment une certaine complexité également.

Jon Skeet
la source
2
Je trouve surprenant que dans un fil sur le rendement, Jon n'ait pas mentionné c.Controls.Count > 0vs .Any():)
tymtam
@Tymek est en fait mentionné dans la réponse liée.
28

Comme Jon Skeet et le colonel Panic le notent dans leurs réponses, l'utilisation yield returnde méthodes récursives peut entraîner des problèmes de performances si l'arbre est très profond.

Voici une méthode d'extension non récursive générique qui effectue une traversée 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 à la solution d'Eric Lippert , RecursiveSelect fonctionne directement avec les énumérateurs de sorte qu'il n'a pas besoin d'appeler Reverse (qui met en mémoire tampon la séquence entière en mémoire).

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

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T);
}
Michael Liu
la source
Pour que cet (excellent) code fonctionne, j'ai dû utiliser 'OfType pour obtenir le ControlCollection sous forme IEnumerable; dans Windows Forms, un ControlCollection n'est pas énumérable: retournez control.Controls.OfType <Control> () .RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()) .Where (c => c est T );
BillW
17

D'autres vous ont fourni la bonne réponse, mais je ne pense pas que votre cas profite du rendement.

Voici un extrait qui réalise la même chose 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>()));
}
tymtam
la source
2
N'utilise pas également LINQ yield? ;)
Philipp M
C'est lisse. J'ai toujours été gêné par la foreachboucle supplémentaire . 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 performances avec les itérateurs que l'utilisation du rendement. @PhilippM: vérification que LINQ utilise yield referencesource.microsoft.com/System.Core/R/…
Herman
Pouce levé pour une excellente solution.
Tomer W
12

Vous devez renvoyer les éléments de l'énumérateur, pas l'énumérateur lui-même, dans votre deuxièmeyield 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
la source
9

Je pense que vous devez renvoyer chacun des contrôles dans les énumérables.

    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
la source
8

La syntaxe de Seredynski est correcte, mais vous devez faire attention à éviter yield returnles fonctions récursives car c'est un désastre pour l'utilisation de la mémoire. Voir https://stackoverflow.com/a/3970171/284795 il évolue de manière explosive avec la profondeur (une fonction similaire utilisait 10% de mémoire dans mon application).

Une solution simple consiste à utiliser une liste et à la transmettre avec la récursivité 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);
    }
}

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

Colonel Panic
la source
0

Bien qu'il existe de nombreuses bonnes réponses, j'ajouterais tout de même qu'il est possible d'utiliser les méthodes LINQ pour accomplir la même chose,.

Par exemple, le code original de l'OP pourrait être réécrit comme:

public static IEnumerable<Control> 
                           GetDeepControlsByType<T>(this Control control)
{
   return control.Controls.OfType<T>()
          .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));        
}
yoel halb
la source
Une solution utilisant cette même approche a été publiée il y a trois ans .
Servy
@Servy Bien qu'il soit similaire (que BTW j'ai manqué entre toutes les réponses ... lors de l'écriture de cette réponse), il est toujours différent, car il utilise .OfType <> pour filtrer, et .Union ()
yoel halb
2
Ce OfTypen'est pas vraiment différent. Au plus, un changement styalistique mineur. Un contrôle ne peut pas être un enfant de plusieurs contrôles, donc l'arborescence parcourue est déjà unqiue. Utiliser à la Unionplace de Concatvérifie inutilement l'unicité d'une séquence déjà garantie d'être unique, et constitue donc un déclassement objectif.
Servy