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

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.