Pregunta

Cuando desea enumerar recursivamente un objeto jerárquico, seleccionando algunos elementos según algunos criterios, existen numerosos ejemplos de técnicas como "aplanar" y luego filtrar usando Linq:como los que se encuentran aquí:

Texto del enlace

Pero, cuando enumeras algo como la colección de Controles de un Formulario o la colección de Nodos de un TreeView, no he podido usar este tipo de técnicas porque parecen requerir un argumento (para el método de extensión) que es un IEnumerable. recopilación :pasar SomeForm.Controls no se compila.

Lo más útil que encontré fue esto:

Texto del enlace

Lo que le brinda un método de extensión para Control.ControlCollection con un resultado IEnumerable que luego puede usar con Linq.

Modifiqué el ejemplo anterior para analizar los nodos de un TreeView sin problemas.

public static IEnumerable<TreeNode> GetNodesRecursively(this TreeNodeCollection nodeCollection)
{
    foreach (TreeNode theNode in nodeCollection)
    {
        yield return theNode;

        if (theNode.Nodes.Count > 0)
        {
            foreach (TreeNode subNode in theNode.Nodes.GetNodesRecursively())
            {
                yield return subNode;
            }
        }
    }
}

Este es el tipo de código que estoy escribiendo ahora usando el método de extensión:

    var theNodes = treeView1.Nodes.GetNodesRecursively();

    var filteredNodes = 
    (
        from n in theNodes
            where n.Text.Contains("1")
                select n
    ).ToList();

Y creo que puede haber una manera más elegante de hacer esto donde se pasan las restricciones.

Lo que quiero saber es si es posible definir dichos procedimientos de manera genérica, de modo que:en tiempo de ejecución puedo pasar el tipo de colección, así como la colección real, a un parámetro genérico, por lo que el código es independiente de si es TreeNodeCollection o Controls.Collection.

También me interesaría saber si hay alguna otra manera (¿más barata?¿más rápido?) que el que se muestra en el segundo enlace (arriba) para obtener un TreeNodeCollection o Control.ControlCollection en un formulario utilizable por Linq.

Un comentario de Leppie sobre 'SelectMany en la publicación SO vinculada primero (arriba) parece una pista.

Mis experimentos con SelectMany han sido:Bueno, llámalos "desastres". :)

Agradezco cualquier consejo.He pasado varias horas leyendo cada publicación que pude encontrar que tocó estas áreas y me duela en exótica como el "combinador Y". Una experiencia "humillante", podría agregar :)

¿Fue útil?

Solución

Este código debería hacer el truco

public static class Extensions
{
    public static IEnumerable<T> GetRecursively<T>(this IEnumerable collection,
        Func<T, IEnumerable> selector)
    {
        foreach (var item in collection.OfType<T>())
        {
            yield return item;

            IEnumerable<T> children = selector(item).GetRecursively(selector);
            foreach (var child in children)
            {
                yield return child;
            }
        }
    }
}

Aquí hay un ejemplo de cómo usarlo

TreeView view = new TreeView();

// ...

IEnumerable<TreeNode> nodes = view.Nodes.
    .GetRecursively<TreeNode>(item => item.Nodes);

Actualización: en respuesta a la publicación de Eric Lippert.

Aquí hay una versión muy mejorada usando la técnica discutida en Todo sobre iteradores .

public static class Extensions
{
    public static IEnumerable<T> GetItems<T>(this IEnumerable collection,
        Func<T, IEnumerable> selector)
    {
        Stack<IEnumerable<T>> stack = new Stack<IEnumerable<T>>();
        stack.Push(collection.OfType<T>());

        while (stack.Count > 0)
        {
            IEnumerable<T> items = stack.Pop();
            foreach (var item in items)
            {
                yield return item;

                IEnumerable<T> children = selector(item).OfType<T>();
                stack.Push(children);
            }
        }
    }
}

Hice una prueba de rendimiento simple usando la siguiente técnica de evaluación comparativa . los resultados hablan por si mismos. La profundidad del árbol solo tiene un impacto marginal en el rendimiento de la segunda solución; mientras que el rendimiento disminuye rápidamente para la primera solución, lo que finalmente conduce a un StackOverflowException cuando la profundidad del árbol se vuelve demasiado grande.

benchmarking

Otros consejos

Parece que estás en el camino correcto y las respuestas anteriores tienen algunas buenas ideas. Pero observo que todas estas soluciones recursivas tienen algunos defectos profundos.

Supongamos que el árbol en cuestión tiene un total de n nodos con una profundidad máxima de árbol de d < = n.

Primero, consumen espacio en la pila del sistema en la profundidad del árbol. Si la estructura de árbol es muy profunda, esto puede volar la pila y bloquear el programa. La profundidad del árbol d es O (lg n), dependiendo del factor de ramificación del árbol. En el peor de los casos, no hay ramificación, solo una lista vinculada, en cuyo caso un árbol con solo unos pocos cientos de nodos volará la pila.

Segundo, lo que está haciendo aquí es construir un iterador que llame a un iterador que llame a un iterador ... de modo que cada MoveNext () en el iterador superior realmente haga una cadena de llamadas que nuevamente sea O (d) en costo. Si hace esto en cada nodo, el costo total en las llamadas es O (nd), que es el peor caso O (n ^ 2) y el mejor caso O (n lg n). Puedes hacerlo mejor que ambos; no hay razón para que esto no pueda ser lineal en el tiempo.

El truco consiste en dejar de usar la pila pequeña y frágil del sistema para realizar un seguimiento de qué hacer a continuación, y comenzar a usar una pila asignada en el montón para hacer un seguimiento explícito.

Debe agregar a su lista de lectura el artículo de Wes Dyer sobre esto:

https: //blogs.msdn. microsoft.com/wesdyer/2007/03/23/all-about-iterators/

Da algunas buenas técnicas al final para escribir iteradores recursivos.

No estoy seguro acerca de TreeNodes, pero puede hacer la colección Controls de un formulario IEnumerable usando System.Linq y, por ejemplo

var ts = (from t in this.Controls.OfType<TextBox>
                 where t.Name.Contains("fish")
                 select t);
//Will get all the textboxes whose Names contain "fish"

Lamento decir que no sé cómo hacer esto recursivo, fuera de mi cabeza.

Basado en la solución de mrydengren:

public static IEnumerable<T> GetRecursively<T>(this IEnumerable collection,
    Func<T, IEnumerable> selector,
    Func<T, bool> predicate)
{
    foreach (var item in collection.OfType<T>())
    {
        if(!predicate(item)) continue;

        yield return item;

        IEnumerable<T> children = selector(item).GetRecursively(selector, predicate);
        foreach (var child in children)
        {
            yield return child;
        }
    }
}


var theNodes = treeView1.Nodes.GetRecursively<TreeNode>(
    x => x.Nodes,
    n => n.Text.Contains("1")).ToList();

Editar: para BillW

Supongo que estás pidiendo algo como esto.

public static IEnumerable<T> <T,TCollection> GetNodesRecursively(this TCollection nodeCollection, Func<T, TCollection> getSub)
 where T, TCollection: IEnumerable
{   
    foreach (var theNode in )
    {
        yield return theNode;
        foreach (var subNode in GetNodesRecursively(theNode, getSub))
        {
            yield return subNode;
        }
    }
}

var all_control = GetNodesRecursively(control, c=>c.Controls).ToList();
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top