web-dev-qa-db-fra.com

IEnumerable et Récursion avec return return

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

La méthode est récursive et j'ai quelques problèmes pour retourner le type que je veux quand le yield return renvoie 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 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 est généré, mais le type incorrect est renvoyé dans la sortie.

Est-il possible d'utiliser yield return en utilisant également la récursivité?

295
Jamie Dixon

Dans une méthode qui retourne IEnumerable<T>, yield return doit renvoyer T, pas un IEnumerable<T>.

Remplacer

yield return c.GetDeepControlsByType<T>();

avec:

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

Vous devez donner 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 que les coûts liés à la récurrence de cette manière sont élevés: vous allez créer un grand nombre d'itérateurs, ce qui peut poser un problème de performances si vous disposez d'une arborescence de contrôle très poussée. Si vous voulez éviter cela, vous devez fondamentalement 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 à états) créé. Voir cette question pour plus de détails et un exemple de mise en œuvre - mais cela ajoute évidemment une certaine complexité.

96
Jon Skeet

Comme Jon Skeet et le colonel Panic le notent dans leurs réponses, en utilisant yield return _ dans les méthodes récursives peut entraîner des problèmes de performances si l’arborescence est très profonde.

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

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);
}
21
Michael Liu

D’autres vous ont donné la bonne réponse, mais je ne pense pas que votre cas tire profit 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>()));
}
16
tymtam

Vous devez renvoyer les items de l'énumérateur, et non de l'énumérateur 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;
            }
        }
    }
}
12
Rob Levine

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;
                }
            }
        }
    }
9
Torbjörn Hansson

la syntaxe de Seredynski est correcte, mais vous devez faire attention à éviter yield return dans les 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é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);
    }
}

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

7
Colonel Panic

Bien qu'il y ait beaucoup de bonnes réponses, je voudrais quand même ajouter qu'il est possible d'utiliser les méthodes LINQ pour accomplir la même chose,.

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

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