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