Перечисление коллекций, которые по своей сути не являются IEnumerable?

Если вы хотите рекурсивно перечислить иерархический объект, выбрав некоторые элементы, основанные на некоторых критериях, существует множество примеров таких методов, как "сглаживание", а затем фильтрация с использованием Linq: например, найденные здесь:

текст ссылки

Но когда вы перечисляете что-то вроде коллекции элементов управления Form или коллекции узлов TreeView, я не смог использовать эти типы методов, потому что они, похоже, требуют аргумента (для метода расширения), который это коллекция IEnumerable: передача в SomeForm.Controls не компилируется.

Самое полезное, что я нашел, это:

текст ссылки

Что дает вам метод расширения для Control.ControlCollection с результатом IEnumerable, который вы можете использовать с Linq.

Я изменил приведенный выше пример, чтобы без проблем проанализировать узлы TreeView.

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;
            }
        }
    }
}

Это код, который я пишу сейчас, используя метод расширения:

    var theNodes = treeView1.Nodes.GetNodesRecursively();

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

И я думаю, что может быть более элегантный способ сделать это, когда передаются ограничения.

Что я хочу знать, если можно определить такие процедуры в общем случае, чтобы: во время выполнения я мог передавать тип коллекции, а также фактическую коллекцию, в общий параметр, поэтому код независимо от того, является ли это TreeNodeCollection или Controls.Collection.

Мне также интересно узнать, есть ли другой способ (более дешевый? fastser?), чем тот, который показан во второй ссылке (выше), чтобы получить TreeNodeCollection или Control.ControlCollection в форме, которую можно использовать Linq.

Комментарий Leppie о "SelectMany" в сообщении SO, связанном с первым (см. выше), похоже на подсказку.

Мои эксперименты с SelectMany были: ну, назовите их "бедствиями".:)

Цените любые указатели. Я потратил несколько часов на чтение каждого сообщения SO, которое я смог найти, касающегося этих областей, и пробрался в такую ​​экзотику, как "y-combinator". "Смиряющий" опыт, я мог бы добавить:)

Ответ 1

Этот код должен делать трюк

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;
            }
        }
    }
}

Вот пример того, как его использовать

TreeView view = new TreeView();

// ...

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

Обновление: В ответ на сообщение Эрика Липперта.

Здесь значительно улучшена версия, использующая технику, описанную в All About Iterators.

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);
            }
        }
    }
}

Я сделал простой тест производительности, используя следующую технику сравнения . Результаты говорят сами за себя. Глубина дерева оказывает незначительное влияние на производительность второго решения; тогда как быстродействие быстро уменьшается для первого решения, в конечном итоге приводя к StackOverflowException, когда глубина дерева становится слишком большой.

benchmarking

Ответ 2

Вы, кажется, на правильном пути, и ответы выше имеют некоторые хорошие идеи. Но я отмечаю, что все эти рекурсивные решения имеют некоторые глубокие недостатки.

Предположим, что рассматриваемое дерево имеет в общей сложности n узлов с максимальной глубиной дерева d <= n.

Прежде всего, они потребляют пространство системного стека в глубине дерева. Если древовидная структура очень глубокая, то это может привести к удалению стека и сбою программы. Глубина дерева d равна O (lg n), в зависимости от коэффициента ветвления дерева. Худший случай - вообще не ветвление - просто связанный список - в этом случае дерево с несколькими сотнями узлов взорвет стек.

Во-вторых, вы создаете итератор, который вызывает итератор, который вызывает итератор... так что каждый MoveNext() в верхнем итераторе фактически выполняет цепочку вызовов, которая снова является O (d) в Стоимость. Если вы сделаете это на каждом node, тогда общая стоимость вызовов будет равна O (nd), что является наихудшим случаем O (n ^ 2) и лучшим случаем O (n lg n). Вы можете сделать лучше, чем оба; нет причин, почему это не может быть линейным во времени.

Трюк состоит в том, чтобы прекратить использование небольшого хрупкого системного стека, чтобы отслеживать, что делать дальше, и начать использовать кусок, выделенный для кучи, чтобы явно отслеживать.

Вы должны добавить в свой список чтения статью Wes Dyer об этом:

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

Он дает некоторые хорошие методы в конце для написания рекурсивных итераторов.

Ответ 3

Я не уверен в TreeNodes, но вы можете сделать коллекцию Controls формы IEnumerable с помощью System.Linq и, например,

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"

Извините, что я не знаю, как сделать это рекурсивным, с верхней части головы.

Ответ 4

Основываясь на решении 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();

Изменить: для BillW

Ответ 5

Я думаю, вы просите что-то вроде этого.

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();