IEnumerable и рекурсия с использованием yield return
у меня есть 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; } } } }
Я думаю,что вы должны дать возврат каждого из элементов управления в перечислимых.
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>())); }