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
в то же время, используя рекурсию?
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;
}
}
}
}
Серединский синтаксис правильный, но вы должны быть осторожны, чтобы избежать yield return
в рекурсивных функциях, потому что это катастрофа для использования памяти. См. /questions/37157433/kogda-ne-ispolzovat-yield-return/37157452#37157452 он масштабируется с глубиной (аналогичная функция использовала 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 для устранения рекурсивных вызовов /questions/15479419/sborka-x86-na-mac/15479431#15479431
Я думаю, что вы должны возвращать каждый из элементов управления в перечислимых.
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;
}
}
}
}
Хотя есть много хороших ответов, я все же добавил бы, что можно использовать методы LINQ для достижения той же цели.
Например, исходный код OP может быть переписан как:
public static IEnumerable<Control>
GetDeepControlsByType<T>(this Control control)
{
return control.Controls.OfType<T>()
.Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));
}