当你想递归地枚举一个分层对象,根据一些标准选择一些元素时,有许多技术的例子,比如“展平”然后使用Linq进行过滤:如下所示:
但是,当你枚举类似Form的Controls集合或TreeView的Nodes集合时,我一直无法使用这些类型的技术,因为它们似乎需要一个参数(对于扩展方法)是一个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?)比第二个链接(上面)所示,以获得Linq可用的形式的TreeNodeCollection或Control.ControlCollection。
Leppie关于'在第一个(上图)链接的SO帖子中的SelectMany的评论似乎是一个线索。
我对SelectMany的实验是:好吧,称之为“灾难”。 :)
欣赏任何指针。我已经花了几个小时阅读我发现的那些触及这些区域的SO帖子,然后漫无目的地进入像“y-combinator”这样的exotica。一个“谦卑”的经历,我可以补充说:)
答案 0 :(得分:31)
此代码应该可以解决问题
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);
更新:回应Eric Lippert的帖子。
这是使用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);
}
}
}
}
我使用以下benchmarking technique进行了简单的性能测试。结果不言自明。树的深度对第二种解决方案的性能只有微不足道的影响;而第一种解决方案的性能迅速下降,当树的深度变得太大时,最终会导致StackOverflowException
。
答案 1 :(得分:22)
您似乎走在正确的轨道上,上面的答案有一些好主意。但我注意到所有这些递归解决方案都有一些深层次的缺陷。
假设所讨论的树总共有n个节点,其最大树深度为d <= n。
首先,它们消耗树深处的系统堆栈空间。如果树结构非常深,那么这可能会破坏堆栈并使程序崩溃。树深d是O(lg n),取决于树的分支因子。更糟糕的情况是根本没有分支 - 只是一个链表 - 在这种情况下,只有几百个节点的树会炸掉堆栈。
其次,你在这里做的是构建一个迭代器,它调用一个调用迭代器的迭代器......这样顶部迭代器上的每个MoveNext()实际上都会执行一个调用链,同样是O(d)in成本。如果在每个节点上执行此操作,则调用的总成本为O(nd),最差情况为O(n ^ 2),最佳情况为O(n lg n)。你可以做得比两者都好;没有理由说这不能及时成为线性的。
诀窍是停止使用小而脆弱的系统堆栈来跟踪下一步该做什么,并开始使用堆分配的堆栈来明确跟踪。
你应该在你的阅读清单上添加Wes Dyer的文章:
https://blogs.msdn.microsoft.com/wesdyer/2007/03/23/all-about-iterators/
他最后给出了一些编写递归迭代器的好技巧。
答案 2 :(得分:2)
我不确定TreeNodes,但你可以使用System.Linq
制作IEnumerable形式的Controls集合,例如
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"
很抱歉,我不知道怎么做这个递归,从头到尾。
答案 3 :(得分:2)
基于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();
编辑:for BillW
答案 4 :(得分:1)
我想你是在问这样的事情。
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();