IEnumerable и Recursion с использованием доходности

307

У меня есть 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
stackoverflow.com/questions/1815497/… : ссылка на ответ «mrydengrens» в теме «Перечисление коллекций, которые по своей природе не являются IEnumerable?» Его пример кода основан на статье в блоге Эрика Липперта, в которой показано, как использовать стеки в рекурсивном перечислении с Linq, что позволяет избежать, возможно, дорогостоящего использования памяти итераторами. имхо очень полезно!
BillW
КСТАТИ. if(c.Controls.Count > 0)-> if(c.Controls.Any()), особенно если вы тоже уступаете :)
tymtam
Я не думаю, что этот случай выигрывает от уступок. Для полноты я предоставил реализацию без yield. Пожалуйста, смотрите ниже :) И это тоже однострочник :)
tymtam
Вы должны быть осторожны, чтобы избежать yield returnв рекурсивных функциях, использование памяти взрывчато. См. Stackoverflow.com/a/30300257/284795
Полковник Паника

Ответы:

485

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

замещать

yield return c.GetDeepControlsByType<T>();

с участием:

foreach (var x in c.GetDeepControlsByType<T>())
{
  yield return x;
}
Марчин Серединский
источник
98

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

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

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

Джон Скит
источник
2
Я нахожу удивительным, что в теме о сдаче Джон не упомянул c.Controls.Count > 0против .Any():)
tymtam
@Tymek на самом деле это упоминается в связанном ответе.
28

Как отмечают Джон Скит и полковник Паник в своих ответах, использование 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();
        }
    }
}

В отличие от решения Эрика Липперта , 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);
}
Майкл Лю
источник
Чтобы заставить этот (отличный) код работать, мне пришлось использовать OfType, чтобы получить ControlCollection в форме IEnumerable; в Windows Forms ControlCollection не перечисляется: return control.Controls.OfType <Control> (). RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()). Где (c => c является T );
BillW
17

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

Вот фрагмент, который достигает того же самого, не уступая.

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
источник
2
Также не использует LINQ yield? ;)
Филипп М
Это гладко Меня всегда беспокоил дополнительный foreachцикл. Теперь я могу сделать это с помощью чисто функционального программирования!
jsuddsjr
1
Мне нравится это решение с точки зрения читабельности, но оно сталкивается с той же проблемой производительности с итераторами, что и при использовании yield. @PhilippM: Подтверждено , что LINQ использует выход referencesource.microsoft.com/System.Core/R/...
Herman
Большой палец вверх для отличного решения.
Томер W
12

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

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

    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;
                }
            }
        }
    }
Торбьерн Ханссон
источник
8

Серединный синтаксис правильный, но вы должны быть осторожны, избегая yield returnрекурсивных функций, потому что это катастрофа для использования памяти. См. Https://stackoverflow.com/a/3970171/284795, он масштабируется с большой глубиной (аналогичная функция использовала 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

Полковник паника
источник
0

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

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

public static IEnumerable<Control> 
                           GetDeepControlsByType<T>(this Control control)
{
   return control.Controls.OfType<T>()
          .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));        
}
Йоэль Хэлб
источник
Решение с использованием того же подхода было опубликовано три года назад .
Servy
@Servy Хотя это похоже (что, кстати, я пропустил во всех ответах ... при написании этого ответа), оно все же отличается, так как использует .OfType <> для фильтрации и .Union ()
yoel halb
2
Это на OfTypeсамом деле совсем не изящное. Максимум незначительных изменений в стиле. Элемент управления не может быть дочерним по отношению к нескольким элементам управления, поэтому пройденное дерево уже не является обязательным. Использование Unionвместо Concatненужной проверки уникальности последовательности, которая уже гарантированно является уникальной, и, следовательно, является объективным понижением.
Servy