IEnumerable и рекурсия с использованием yield return
у меня есть IEnumerable<T>
метод, который я использую для поиска элементов управления на странице WebForms.
метод рекурсивен, и у меня возникли проблемы с возвратом типа, который я хочу, когда yield return
это returnig значение рекурсивного вызова.
мой код выглядит следующим образом:
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>();
}
}
}
в настоящее время возникает ошибка" не удается преобразовать тип выражения". Если однако этот метод возвращает type IEnumerable<Object>
, код строится, но неправильный тип возвращается в выход.
есть ли способ использовать yield return
в то же время, используя рекурсию?
8 ответов
внутри метода, который возвращает IEnumerable<T>
, yield return
вернуться T
, а не IEnumerable<T>
.
заменить
yield return c.GetDeepControlsByType<T>();
С:
foreach (var x in c.GetDeepControlsByType<T>())
{
yield return x;
}
нужно выход каждый из пунктов, полученный рекурсивным вызовом:
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;
}
}
}
}
обратите внимание, что есть стоимость рекурсии таким образом - вы в конечном итоге создадите много итераторов, которые могут создать проблему производительности, если у вас есть действительно глубокое дерево управления. Если вы хотите избежать этого, Вам в основном нужно сделать рекурсию самостоятельно в рамках метода, чтобы убедиться, что создан только один итератор (государственная машина). См.этот вопрос дополнительные детали и пример реализации - но это, очевидно, добавляет определенную сложность.
как Джон Скит и полковник паника отмечают в своих ответах, используя 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);
}
другие предоставили вам правильный ответ, но я не думаю, что ваш случай выигрывает от уступки.
вот фрагмент, который достигает того же, не уступая.
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>()));
}
вам нужно вернуть предметы из перечислителя, а не самого перечислителя, в вашем втором 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;
}
}
}
}
Я считаю, что нужно вернуть выход каждого элемента управления в enumerables.
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;
}
}
}
}
Щербединской это - это правильно, но вы должны быть осторожны, чтобы избежать 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
хотя есть много хороших ответов, я бы все равно добавил, что можно использовать методы LINQ для достижения того же самого .
например, исходный код OP может быть переписан как:
public static IEnumerable<Control>
GetDeepControlsByType<T>(this Control control)
{
return control.Controls.OfType<T>()
.Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));
}