IEnumerable i Rekurencja za pomocą zwrotu plonu

Mam metodę IEnumerable<T>, której używam do znajdowania kontrolek na stronie formularzy internetowych.

Metoda jest rekurencyjna i mam pewne problemy ze zwróceniem typu, który chcę, gdy yield return Zwraca wartość wywołania rekurencyjnego.

Mój kod wygląda następująco:

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

To powoduje błąd "nie można przekonwertować typu wyrażenia". Jeśli jednak ta metoda zwraca typ IEnumerable<Object>, kod jest budowany, ale na wyjściu zwracany jest niewłaściwy typ.

Czy istnieje sposób użycia yield return Podczas używania rekurencji?

Author: Jamie Dixon, 2010-01-13

8 answers

Wewnątrz metody, która zwraca IEnumerable<T>, yield return musi zwrócić T, a nie IEnumerable<T>.

Zastąp

yield return c.GetDeepControlsByType<T>();

Z:

foreach (var x in c.GetDeepControlsByType<T>())
{
  yield return x;
}
 456
Author: Marcin Seredynski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-13 10:29:49

Musisz oddać każdy z elementów wywołanych wywołaniem rekurencyjnym:

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

Zauważ, że rekursowanie w ten sposób jest kosztowne - w końcu stworzysz wiele iteratorów, które mogą powodować problemy z wydajnością, jeśli masz naprawdę głębokie drzewo kontroli. Jeśli chcesz tego uniknąć, w zasadzie musisz wykonać rekursję samodzielnie w metodzie, aby upewnić się, że istnieje tylko jeden iterator (maszyna stanowa). Zobacz to pytanie aby uzyskać więcej szczegółów i próbkę implementacja - ale to oczywiście dodaje pewną złożoność zbyt.

 92
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:45

Jak Jon Skeet i Pułkownik Panic zauważają w swoich odpowiedziach, używanie yield return w metodach rekurencyjnych może powodować problemy z wydajnością, jeśli drzewo jest bardzo głębokie.

Oto ogólna nie-rekurencyjna metoda rozszerzenia, która wykonuje trawersację w pierwszej głębokości sekwencji drzew:

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

W Przeciwieństwie Do rozwiązania Erica Lipperta , RecursiveSelect działa bezpośrednio z enumeratorami, dzięki czemu nie musi wywoływać odwrotności (co buforuje całą sekwencję w pamięci).

Za pomocą RecursiveSelect, oryginalną metodę OP można przepisać w następujący sposób:

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T);
}
 17
Author: Michael Liu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-13 12:40:36

Inni dostarczyli Ci poprawną odpowiedź, ale nie sądzę, że Twoja sprawa przyniesie korzyści z poddania się.

Oto fragment, który osiąga to samo bez ulegania.

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>()));
}
 14
Author: tymtam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-15 09:43:03

Musisz zwrócić pozycje z wyliczenia, a nie samego wyliczenia, w drugim 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
Author: Rob Levine,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-13 10:30:39

/ Align = "left" /

    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
Author: Torbjörn Hansson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-13 10:32:02

Składnia Seredyńskiego jest poprawna, ale należy uważać, aby unikać yield return w funkcjach rekurencyjnych, ponieważ jest to katastrofa dla wykorzystania pamięci. Zobacz https://stackoverflow.com/a/3970171/284795 skaluje się wybuchowo z głębią (podobną funkcją było wykorzystanie 10% pamięci w mojej aplikacji).

Prostym rozwiązaniem jest użycie jednej listy i podanie jej z rekurencją 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);
    }
}

Alternatywnie możesz użyć stosu i pętla while eliminująca wywołania rekurencyjne https://codereview.stackexchange.com/a/5661/754

 6
Author: Colonel Panic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:34

Chociaż istnieje wiele dobrych odpowiedzi, to jednak dodam, że możliwe jest użycie metod LINQ, aby osiągnąć to samo,.

Na przykład oryginalny kod OP można przepisać jako:

public static IEnumerable<Control> 
                           GetDeepControlsByType<T>(this Control control)
{
   return control.Controls.OfType<T>()
          .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));        
}
 0
Author: yoel halb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-21 15:07:20