IEnumerable y Recursion usando rendimiento
Tengo un IEnumerable<T>
método que estoy usando para buscar controles en una página de formularios web.
El método es recursivo y tengo algunos problemas para devolver el tipo que quiero cuando yield return
devuelve el valor de la llamada recursiva.
Mi código tiene el siguiente aspecto:
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>();
}
}
}
Actualmente, esto genera el error "No se puede convertir el tipo de expresión". Sin embargo, si este método devuelve el tipo IEnumerable<Object>
, el código se compila, pero se devuelve el tipo incorrecto en la salida.
¿ Hay alguna forma de utilizarlo yield return
y al mismo tiempo utilizar la recursividad?
Dentro de un método que regresa IEnumerable<T>
, yield return
tiene que regresar T
, no un IEnumerable<T>
.
Reemplazar
yield return c.GetDeepControlsByType<T>();
con:
foreach (var x in c.GetDeepControlsByType<T>())
{
yield return x;
}
Debe generar cada uno de los elementos generados por la llamada recursiva:
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;
}
}
}
}
Tenga en cuenta que recurrir de esta manera tiene un costo: terminará creando muchos iteradores, lo que puede crear un problema de rendimiento si tiene un árbol de control realmente profundo. Si desea evitar eso, básicamente necesita realizar la recursividad usted mismo dentro del método, para asegurarse de que solo se haya creado un iterador (máquina de estados). Consulte esta pregunta para obtener más detalles y una implementación de muestra, pero esto obviamente también agrega cierta complejidad.
Como señalan Jon Skeet y Colonel Panic en sus respuestas, el uso yield return
de métodos recursivos puede causar problemas de rendimiento si el árbol es muy profundo.
A continuación se muestra un método de extensión genérico no recursivo que realiza un recorrido en profundidad de una secuencia de árboles:
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();
}
}
}
A diferencia de la solución de Eric Lippert , RecursiveSelect trabaja directamente con enumeradores para que no necesite llamar a Reverse (que almacena toda la secuencia en la memoria).
Usando RecursiveSelect, el método original del OP se puede reescribir simplemente así:
public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T);
}
Otros le dieron la respuesta correcta, pero no creo que su caso se beneficie de ceder.
Aquí hay un fragmento que logra lo mismo sin ceder.
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>()));
}
Debe devolver los elementos del enumerador, no del enumerador en sí, en su segundayield 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;
}
}
}
}