IEnumerable y Recursion usando yield return

Tengo un IEnumerable que estoy usando para buscar controles en una página de WebForms.

El método es recursivo y tengo algunos problemas para devolver el tipo que quiero cuando el yield return el valor de la llamada recursiva.

Mi código se ve de la siguiente manera:

  public static IEnumerable GetDeepControlsByType(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(); } } } 

Esto arroja actualmente un error de “No se puede convertir el tipo de expresión”. Sin embargo, si este método devuelve el tipo IEnumerable , el código se genera, pero el tipo incorrecto se devuelve en la salida.

¿Hay alguna manera de usar el yield return al mismo tiempo que se usa la recursión?

Dentro de un método que devuelve IEnumerable , yield return tiene que devolver T , no un IEnumerable .

Reemplazar

 yield return c.GetDeepControlsByType(); 

con:

 foreach (var x in c.GetDeepControlsByType()) { yield return x; } 

Debe ceder cada uno de los elementos generados por la llamada recursiva:

 public static IEnumerable GetDeepControlsByType(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()) { yield return control; } } } } 

Tenga en cuenta que hay un costo de recurrencia de esta manera: terminará creando muchos iteradores, lo que puede crear un problema de rendimiento si tiene un árbol de control realmente profundo. Si quiere evitar eso, básicamente necesita hacer la recursión usted mismo dentro del método, para asegurarse de que solo 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 Jon Skeet y el coronel Panic apuntan en sus respuestas, usar el yield return en métodos recursivos puede causar problemas de rendimiento si el árbol es muy profundo.

Aquí hay un método de extensión genérico no recursivo que realiza un recorrido en profundidad de una secuencia de árboles:

 public static IEnumerable RecursiveSelect( this IEnumerable source, Func> childSelector) { var stack = new Stack>(); 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 los enumeradores para que no necesite llamar a Reverse (que almacena la secuencia completa en la memoria).

Usando RecursiveSelect, el método original de OP puede reescribirse simplemente así:

 public static IEnumerable GetDeepControlsByType(this Control control) { return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T); } 

Otros le proporcionaron la respuesta correcta, pero no creo que su caso se beneficie al ceder.

Aquí hay un fragmento que logra lo mismo sin ceder.

 public static IEnumerable GetDeepControlsByType(this Control control) { return control.Controls .Where(c => c is T) .Concat(control.Controls .SelectMany(c =>c.GetDeepControlsByType())); } 

Debe devolver los elementos del enumerador, no del enumerador en sí, en su segundo yield return

 public static IEnumerable GetDeepControlsByType(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()) { yield return ctrl; } } } } 

Creo que debe devolver cada uno de los controles en los enumerables.

  public static IEnumerable GetDeepControlsByType(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()) { yield return childControl; } } } } 

La syntax de Seredynski es correcta, pero debe tener cuidado para evitar el yield return en las funciones recursivas porque es un desastre para el uso de la memoria. Ver https://stackoverflow.com/a/3970171/284795 se escala explosivamente con la profundidad (una función similar fue el uso de 10% de memoria en mi aplicación).

Una solución simple es usar una lista y pasarla con la recursión https://codereview.stackexchange.com/a/5651/754

 ///  /// Append the descendents of tree to the given list. ///  private void AppendDescendents(Tree tree, List descendents) { foreach (var child in tree.Children) { descendents.Add(child); AppendDescendents(child, descendents); } } 

De forma alternativa, podría usar una stack y un ciclo while para eliminar las llamadas recursivas https://codereview.stackexchange.com/a/5661/754

Si bien hay muchas buenas respuestas, aún agregaría que es posible usar métodos LINQ para lograr lo mismo,.

Por ejemplo, el código original del PO podría reescribirse como:

 public static IEnumerable GetDeepControlsByType(this Control control) { return control.Controls.OfType() .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType())); }