这是在数组或数据项列表的循环中调用 Dispatcher.BeginInvoke/lambda 的正确方法吗?

本文关键字:lambda BeginInvoke 方法 Dispatcher 数组 数据项 循环 列表 调用 | 更新日期: 2023-09-27 18:34:31

我继承了一些使用 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来重新计算哪个根。我应该使用的内容。

我会认为(在我之前编写代码的人显然认为(所使用的变量的值将在创建时计算并存储以供 lambda 使用,而不是在 BeginInvoke 开始工作时。

在循环中可靠地使用 lambda 的 BeginInvoke 是否正确? 还是我偏离了目标?


更新

对于每个变量只能由 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-capturing-a-foreach-iteration-variable-in-a-lambda-expression/

这是在数组或数据项列表的循环中调用 Dispatcher.BeginInvoke/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 中正确捕获。

http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx