[英]Is this the correct way to call Dispatcher.BeginInvoke/lambda in a loop over an array or list of data items
我继承了一些代码,这些代码使用BeginInkoke将选项卡添加到TabControl中,如下所示:
foreach (DitaNestedContent content in root.Content)
{
CrlList.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action<TabControl>((tabControl) =>
{
TabItem aTab = new TabItem();
if (content.Paths != null)
{
PublicationsListUserControl crlTree = new PublicationsListUserControl(content.Path, filename);
crlTree.MinWidth = 5;
aTab.Content = crlTree;
}
aTab.Header = content.Name;
tabControl.Items.Add(aTab);
}), CrlList);
}
这一直有效,直到我重建项目后,仍创建了正确数量的选项卡,但每个选项卡都包含最后一个选项卡的内容(仅)。 我认为时间已更改,并且先前的代码只是偶然地工作,并且第一个BeginInvoke现在仅在循环完成之后才启动,因此, content
在运行时等于最后一个值。
因此,我决定重写代码,但对于最终似乎可行的方法感到惊讶:
List<String> contentPaths = new List<string>();
foreach (DitaNestedContent content in root.Content)
{
contentPaths.Add(String.Copy(content.Path));
}
for (Int32 i = 0; i < root.Content.Count; ++i)
{
CrlList.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action<TabControl>((tabControl) =>
{
if (i >= root.Content.Count) { i = 0; }
TabItem aTab = new TabItem();
if (contentPaths[i] != null)
{
String contentPath = contentPaths[i];
PublicationsListUserControl crlTree = new PublicationsListUserControl(contentPath, filename);
crlTree.MinWidth = 5;
aTab.Content = crlTree;
}
aTab.Header = root.Content[i].Name;
tabControl.Items.Add(aTab);
++i;
}), CrlList);
}
基本上,不是使用当前content
来调用PublicationsListUserControl
构造函数,而是在lambda中使用i
来重新计算应该使用哪个root.Content。
我本来以为(以及在我之前写过代码的人显然认为),所使用的变量的值应该是在创建lambda时计算出来并存储以供lambda使用的,而不是在BeginInvoke开始工作时才使用。
在循环中可靠地使用带有lambda的BeginInvoke正确吗? 还是我偏离标准?
更新
foreach变量只能由C#5.0捕获,请参见此处:
http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/ http://csharp.2000things.com/2014/09/19/1186-在lambda表达式中捕获每个迭代变量/
关于root.Content
枚举是否有些慢? 即检索到的每个新值都需要一些时间? 如果没有,那么实际上没有充分的理由对枚举中的每个项目都执行新的调用。 该代码确实应该将循环放入被调用的方法中。 即:
CrlList.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
(TabControl tabControl) =>
{
foreach (DitaNestedContent content in root.Content)
{
TabItem aTab = new TabItem();
if (content.Paths != null)
{
PublicationsListUserControl crlTree =
new PublicationsListUserControl(content.Path, filename);
crlTree.MinWidth = 5;
aTab.Content = crlTree;
}
aTab.Header = content.Name;
tabControl.Items.Add(aTab);
}
}), CrlList);
如果确实有充分的理由在调用之外执行foreach
,那么您就需要更加努力地确定确切的原因,为什么原始代码不符合您的要求。 您原来的理论是不正确的,并且新代码比原始代码的错误更残破。
一foreach
循环变量基本上每个循环迭代重新创建的,所以它是安全的捕捉变量本身,而for
为整个回路形成循环变量一次,因此并不安全捕获。 换句话说,您的理论认为content
变量由每个调用的方法实例共享。 它们各自获得变量的私有副本,因此实际执行被调用的方法并不重要。
通过捕获该变量并在调用的匿名方法中递增该变量,您尝试解决单个for
循环变量肯定可以在某些情况下起作用。 但是,您同时遇到了种族问题和线程安全问题,因为调用BeginInvoke()
方法的循环正在修改该变量,该变量也在执行被调用方法的UI线程中使用。 如果在循环完成之前执行一个或多个调用,则循环本身将增加变量以及被调用的方法,从而使您绕过某些索引,重复其他索引,最糟糕的是,可能拥有索引在处理单个元素的中间更改权利。
解决在for
循环中进行捕获的通常方法是拥有一个局部变量,将循环变量复制到该局部变量中,并使用该变量而不是循环变量。 例如:
for (int i = 0; i < max; i++)
{
int localIndex = i;
Dispatcher.Current.BeginInvoke(() => /* do something with localIndex, not i */);
}
但是,您实际上不需要使用for
循环。 正如我所说,在涉及变量捕获时, foreach
特别安全,因此,无论您看到什么错误行为,它都不是捕获错误。
(或者,如果是的话,您发布的代码不是实际执行的代码。请考虑发布一个好的代码示例 ,该示例完整,简洁并且可靠地再现了您遇到的问题)。
这里可能发生的情况是,您从代码作者那里移走了一个编译器版本,在C#5之前,foreach主体中定义的变量(您的内容变量)是在循环外部定义的,并且取了最后一个适用的值,在C#5之后,该变量在循环内部定义,并在lambda中被适当捕获。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.