枚举本质上不是 IEnumerable 的集合?
-
06-07-2019 - |
题
当您想要递归枚举分层对象,根据某些条件选择某些元素时,有许多技术示例,例如“展平”,然后使用 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 的实验是:好吧,称他们为“灾难”。 :)
感谢任何指点。我花了几个小时的阅读,所以我可以找到涉及这些领域的帖子,然后漫步到诸如“ Y-Combinator”之类的奇特加。我可能会补充的“谦卑”经历:)
解决方案
这段代码应该可以解决问题
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);
更新: 回应埃里克·利珀特的帖子。
这是使用中讨论的技术的一个大大改进的版本 关于迭代器的一切.
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
当树的深度变得太大时。
其他提示
您似乎走在正确的道路上,上面的答案有一些好主意。但我注意到所有这些递归解决方案都有一些深刻的缺陷。
假设所讨论的树总共有 n 个节点,最大树深度为 d <= n。
首先,它们消耗树深度的系统堆栈空间。如果树结构非常深,那么这可能会破坏堆栈并使程序崩溃。树深度 d 为 O(lg n),取决于树的分支因子。更糟糕的情况是根本没有分支——只有一个链表——在这种情况下,只有几百个节点的树将破坏堆栈。
其次,您在这里所做的是构建一个迭代器,该迭代器调用一个迭代器,该迭代器调用一个迭代器......这样顶部迭代器上的每个 MoveNext() 实际上都会执行一系列调用,成本又是 O(d)。如果在每个节点上执行此操作,则调用的总成本为 O(nd),最坏情况为 O(n^2),最好情况为 O(n lg n)。你可以比两者都做得更好;没有理由不能在时间上呈线性。
诀窍是停止使用小型、脆弱的系统堆栈来跟踪下一步要做什么,并开始使用堆分配的堆栈来显式跟踪。
您应该将 Wes Dyer 的相关文章添加到您的阅读列表中:
https://blogs.msdn.microsoft.com/wesdyer/2007/03/23/all-about-iterators/
他最后给出了一些编写递归迭代器的好技巧。
我不确定 TreeNodes,但您可以使用 IEnumerable 形式创建 Controls 集合 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"
抱歉,我不知道如何进行递归,这是我的想法。
基于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();
编辑:为比尔W
我猜你正在要求这样的东西。
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();