使用 Yield Return 的高效通用递归迭代器





4.00/5 (19投票s)
本文演示了如何使用泛型和 yield return 来优化递归迭代容器控件的常见编程任务。
引言
开发人员经常执行的操作是递归地遍历控件容器(如 Form
或 Panel
)中的所有控件及其所有子容器。
我在这里列出的代码是一种简单但非常有效的通用方法。 这绝不是一个震惊世界的革命性突破,但它的纯粹简单性很好!
任务
假设发生了无效命令,您希望将表单上的所有 TextBox
设置为显示 "n/a"。 因此,您将不得不递归地查找此 Form
容器及其可能包含的所有其他子容器中的所有 TextBox
。
一种效率低下但经常使用的方法
一种常用的方法是创建一个单独的例程,该例程返回一个 TextBox
控件列表,然后可以在调用例程中将其设置为 "n/a"。 像这样...
public static List<Control> GetControlsOfType(Control ctrMain)
{
List<Control> list = new List<Control>();
foreach (Control c in ctrMain.Controls)
{
if (c.GetType().Equals(typeof(TextBox)))
list.Add(c);
if (c.Controls.Count > 0)
list.AddRange(GetControlsOfType(c));
}
return list;
}
... 像这样使用
private void button1_Click(object sender, EventArgs e)
{
foreach (Control c in Utils.GetControlsOfType(this))
c.Text = "n/a";
}
这种技术有效,但有一些丑陋的缺点
- 如果需要查找
Form
中的所有Button
而不是TextBox
怎么办? 您将不得不复制您的代码或在其中放置一个 switch 来区分控件类型。 - 您正在创建一个新的
List
,该列表可能仅使用一次,之后它将被指定用于垃圾回收。 如果您经常这样做,并且可能涉及更多项目,那么这可能会对您的内存占用和代码效率产生真正的影响。 请记住,毕竟您正在创建并填充一个包含所有项目的List
,只是为了设置一个Text
属性,之后您不再需要此长列表并将其dispose 掉!!
通用 IEnumerable 方法
现在看看下面的代码。 通过使用泛型,我们解决了绑定到特定 Type
的第一个问题。 现在,在调用该方法时,我们必须通过指定 <T>
来指定我们要搜索的 Type
。
然后,通过使用 yield return 语句并返回通用 IEnumerable<T>
而不是完全填充的临时 List
,我们能够显着减少内存占用并使代码更高效!!
public delegate bool delTypeCheck(Type type);
/// <summary>
/// Examines the full content of this control's Controls
/// collection and returns the enumerator instead of a generic list.
/// </summary>
/// <typeparam name="T">The Type we are looking for -
/// we will also treat any derived types as a match!</typeparam>
/// <param name="ctrMain">The control to examine.</param>
/// <param name="IsExemptCheck">A delegate allowing us to specify
/// which controls to exempt from examining.</param>
/// <returns>The actual enumerator allowing us to NOT have
/// to create a helper intermediate list.</returns>
public static IEnumerable<T> GetControlsOfType<T>
(Control ctrMain, delTypeCheck IsExemptCheck) where T : class
{
// Determine the Type we need to look out for
Type searchType = typeof(T);
foreach (Control c in ctrMain.Controls)
{
// If user wants to exclude certain types then a call-back has been given
// So call it with the current control-type and check if it is exempt.
if (IsExemptCheck != null && IsExemptCheck(c.GetType()))
continue; // the type of c is exempt so continue.
// If a match is found then yield this item back directly
if (c is T) yield return (c as T);
// if you want to search for specific Types only (and NOT derived types) then
// uncomment the following lines (and comment out the above statement).
//if (c.GetType().Equals(searchType))
// yield return (c as T);
// If the control hosts other controls then recursively call this function again.
if (c.Controls.Count > 0)
foreach (T t in GetControlsOfType<T>(c, IsExemptCheck))
yield return t;
}
}
它是这样使用的。
如果我们要找到所有派生自 TextBoxBase
的控件(包括 TextBox
和 RichTextBox
)并将所有这些控件的 Text
属性设置为 "n/a"
,那么请实现如下所示的代码
foreach (Control c in Utils.GetControlsOfType<TextBoxBase>(this, null))
{
c.Text = "n/a";
}
此外,排除某些类型进行检查。
假设您要排除搜索某些类型的容器(在本例中为 Panel
),那么您可以提供一个匿名 delegate
来排除这些容器(请参见下面的代码)。 'IsExemptCheck
' delegate
传递正在检查的 Type
,并要求您返回是否应该排除它。 这为调用者提供了更大的范围,也可以指向一个更复杂的功能来确定是否检查控件。 在这种情况下,我们只想排除 Panel
类型,因此一个简单的匿名 delegate
就足够了。
foreach (Control c in Utils.GetControlsOfType<TextBoxBase>
(this, delegate(Type MyType) { return MyType.Equals(typeof(Panel)); }))
{
c.Text = "n/a";
}
请注意:不要忘记约束 'where T : class'
。 我们将其放在上面是为了确保我们仅处理引用类型。 否则,语句 'yield return (c as T)'
将失败,因为将控件 c
强制转换为类型 T
显然只能与 Control 类型(引用类型)一起发生!
Just a Word (随便说几句)
通过这个例子,我只是想传达一种不同的看待解决此类任务的方式。
在 Form
中使用它来查找 TextBox
,您可能不会直接注意到速度或内存的改善,但请再次尝试将其用于包含成千上万个条目的 List
(s) 迭代,您很快就会发现差异! :)
历史
- 2007 年 6 月 1 日:文章发布
- 2007 年 6 月 6 日:文章更新 - 语句更改为 (
c
isT
) - 感谢 PIEBALDconsult!