Перечисление коллекций, которые по своей сути не являются IEnumerable?
Когда вы хотите рекурсивно перечислить иерархический объект, выбирая некоторые элементы на основе некоторых критериев, существует множество примеров таких методов, как "выравнивание", а затем фильтрация с использованием Linq: например, найденных здесь:
Но когда вы перечисляете что-то вроде коллекции Controls формы или коллекции Nodes TreeView, я не смог использовать эти типы методов, потому что они, кажется, требуют аргумента (для метода расширения), который является IEnumerable collection: передача в 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.
Мне также было бы интересно узнать, есть ли другой способ (дешевле? Быстрее?), Чем тот, который показан во второй ссылке (выше), для получения TreeNodeCollection или Control.ControlCollection в форме, используемой Linq.
Комментарий Леппи о "SelectMany" в посте SO, связанном с первым (выше), выглядит как подсказка.
Мои эксперименты с SelectMany были: ну, назовите их "бедствия".:)
Цените любые указатели. Я потратил несколько часов, читая каждый SO-пост, который касался этих областей, и пробирался к такой экзотике, как "y-combinator"."Унизительный" опыт, я мог бы добавить:)
5 ответов
Этот код должен сделать свое дело
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);
}
}
}
}
Я сделал простой тест производительности, используя следующую технику бенчмаркинга. Результаты говорят сами за себя. Глубина дерева оказывает лишь незначительное влияние на производительность второго решения; в то время как производительность первого решения быстро снижается, что в конечном итоге приводит к StackruException
когда глубина дерева становится слишком большой.
Вы, кажется, находитесь на правильном пути, и у ответов выше есть некоторые хорошие идеи. Но я отмечаю, что все эти рекурсивные решения имеют некоторые глубокие недостатки.
Предположим, что у рассматриваемого дерева есть всего n узлов с максимальной глубиной дерева d <= n.
Во-первых, они занимают место в системном стеке в глубине дерева. Если древовидная структура очень глубокая, это может привести к перегрузке стека и падению программы. Глубина дерева d составляет O(lg n), в зависимости от коэффициента ветвления дерева. В худшем случае это вообще не ветвление - это просто связанный список, и в этом случае дерево, содержащее всего несколько сотен узлов, разрушит стек.
Во-вторых, то, что вы делаете здесь, - это создание итератора, который вызывает итератор, который вызывает итератор... так, чтобы каждый MoveNext() на верхнем итераторе фактически выполнял цепочку вызовов, стоимость которых снова равна O(d). Если вы делаете это на каждом узле, то общая стоимость вызовов составляет O(nd), что является наихудшим случаем O(n^2) и наилучшим случаем O(n lg n). Вы можете сделать лучше, чем оба; нет никаких причин, почему это не может быть линейным во времени.
Хитрость заключается в том, чтобы прекратить использование небольшого хрупкого системного стека, чтобы отслеживать, что делать дальше, и начать использовать выделенный в куче стек для явного отслеживания.
Вы должны добавить в свой список чтения статью Уэса Дайера об этом:
https://blogs.msdn.microsoft.com/wesdyer/2007/03/23/all-about-iterators/
В конце он дает несколько хороших приемов написания рекурсивных итераторов.
Я не уверен насчет 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"
Сожалею, что не знаю, как сделать это рекурсивным, от макушки головы.
На основании решения Мриденгрена:
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
Я думаю, вы просите что-то вроде этого.
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();