IEnumerable и Recursion с возвратом доходности

У меня есть метод IEnumerable<T>, который я использую для поиска элементов управления на странице WebForms.

Метод рекурсивный, и у меня возникают некоторые проблемы с возвратом типа, который я хочу, когда 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)
            {
                yield return c.GetDeepControlsByType<T>();
            }
        }
    }

В настоящее время выбрасывается ошибка "Невозможно преобразовать тип выражения". Если, однако, этот метод возвращает тип IEnumerable<Object>, код строит, но на выходе возвращается неправильный тип.

Есть ли способ использования yield return, в то же время используя рекурсию?

Ответ 1

Внутри метода, который возвращает IEnumerable<T>, yield return должен возвращать T, а не IEnumerable<T>.

Заменить

yield return c.GetDeepControlsByType<T>();

с:

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

Ответ 2

Вам нужно предоставить каждый элемент, полученный рекурсивным вызовом:

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

Обратите внимание на то, что существует такая возможность для рекурсии - вы создадите много итераторов, которые могут создать проблему производительности, если у вас есть действительно глубокое дерево управления. Если вы хотите этого избежать, вам в основном нужно сделать рекурсию самостоятельно в рамках метода, чтобы убедиться, что создан только один итератор (конечный автомат). См. этот вопрос для получения более подробной информации и примерной реализации - но это, очевидно, также добавляет определенную сложность.

Ответ 3

Как отмечают в своих ответах Джон Скит и Полковник Паник, использование yield return в рекурсивных методах может вызвать проблемы с производительностью, если дерево очень глубокое.

Здесь приведен общий нерекурсивный метод расширения, который выполняет обход по глубине последовательности деревьев:

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

В отличие от решения Eric Lippert, RecursiveSelect работает напрямую с счетчиками, так что ему не нужно вызывать Reverse (который буферизует всю последовательность в памяти).

Используя RecursiveSelect, исходный метод OP можно переписать следующим образом:

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

Ответ 4

Другие предоставили вам правильный ответ, но я не думаю, что ваше дело принесет выгоду.

Вот фрагмент, который достигает одинакового результата без урона.

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

Ответ 5

Вам нужно вернуть элементы из перечисления, а не самого счетчика, во второй 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;
            }
        }
    }
}

Ответ 6

Я думаю, вам нужно вернуть каждый элемент управления в перечислениях.

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

Ответ 7

синтаксис Seredynski является правильным, но вы должны быть осторожны, чтобы избежать yield return в рекурсивных функциях, потому что это катастрофа для использования памяти. См. fooobar.com/questions/25957/..., он весит весом с глубиной (аналогичная функция использовала 10% памяти в моем приложении).

Простое решение - использовать один список и передать его с рекурсией 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);
    }
}

В качестве альтернативы вы можете использовать стек и цикл while для устранения рекурсивных вызовов https://codereview.stackexchange.com/a/5661/754

Ответ 8

Пока есть много хороших ответов, я бы добавил, что можно использовать методы LINQ для выполнения того же самого.

Например, исходный код OP можно переписать как:

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