IEnumerable y Recursion usando rendimiento

Resuelto Jamie Dixon asked hace 14 años • 8 respuestas

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 returndevuelve 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 returny al mismo tiempo utilizar la recursividad?

Jamie Dixon avatar Jan 13 '10 17:01 Jamie Dixon
Aceptado

Dentro de un método que regresa IEnumerable<T>, yield returntiene que regresar T, no un IEnumerable<T>.

Reemplazar

yield return c.GetDeepControlsByType<T>();

con:

foreach (var x in c.GetDeepControlsByType<T>())
{
  yield return x;
}
Marcin Seredynski avatar Jan 13 '2010 10:01 Marcin Seredynski

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.

Jon Skeet avatar Jan 13 '2010 10:01 Jon Skeet

Como señalan Jon Skeet y Colonel Panic en sus respuestas, el uso yield returnde 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);
}
Michael Liu avatar May 25 '2015 15:05 Michael Liu

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>()));
}
tymtam avatar Aug 11 '2013 14:08 tymtam

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;
            }
        }
    }
}
Rob Levine avatar Jan 13 '2010 10:01 Rob Levine