【问题标题】:Subscribing to a event in a closure while iterating through a collection results in the last item subscribing to the event [duplicate]在遍历集合时订阅闭包中的事件会导致最后一个项目订阅该事件[重复]
【发布时间】:2014-02-11 06:03:59
【问题描述】:

代码:

using System;
using System.Collections.Generic;

namespace so {

   public abstract class Feature {
      public void doIt() {
         Console.WriteLine( GetType().FullName );
      }
   }

   class A : Feature { }
   class B : Feature { }
   class C : Feature { }

   public class SSCCE {

      event EventHandler Click;

      static void Main( string[] args ) {
         SSCCE sscce = new SSCCE();
         List<Feature> features = new List<Feature>();
         features.Add( new A());
         features.Add( new B() );
         features.Add( new C() );
         foreach ( Feature feature in features ) {
            sscce.Click += ( object sender, EventArgs e ) => { feature.doIt(); };
         }
         sscce.Click.Invoke( null, null );
      }
   }
}

预期结果:

so.A
so.B
so.C

观察结果:

so.C
so.C
so.C

在 java 中,foreach 循环中 Feature 前面的 final 关键字允许在 .doIt() 之前的 lambda 动作的主体中使用 feature 值。

C#中有什么好的语法?

【问题讨论】:

  • 你使用的是什么版本的 C#?
  • 我使用的是 Microsoft Visual C# 2010,编译器在哪里?它叫什么名字?
  • Jon Skeet 有一个有用的页面。长话短说,您可能使用的是 C# 4.0 或更早版本:csharpindepth.com/Articles/Chapter1/Versions.aspx 此修复直到 C# 5 才引入。

标签: c# linq foreach


【解决方案1】:

最后一个特征被你的 lambda 捕获(它是一个闭包)。您应该创建局部变量以在每次迭代中捕获特征:

 foreach (Feature feature in features) {
    Feature current = feature;
    sscce.Click += (object sender, EventArgs e) => { current.doIt(); };
 }

我建议您阅读 Eric Lippert 博客上的 Closing over the loop variable 文章。

注意:这在 C# 的最新版本中已修复


要了解会发生什么,让我们看看在您的案例中生成了哪些代码(之前的 C# 5)。因此,您的 lambda 使用局部变量,仅生成方法是不够的 - 编译器生成捕获 lambda 中使用的局部变量的私有类:

private sealed class AnonymousClass
{
    public Feature feature;

    public void AnonymousMethod(object sender, EventArgs e)
    {
        this.feature.doIt();
    }
}

并且您的代码已修改,以便它使用此 AnonymousClass 的实例并将其 AnonymousMethod 订阅到 Click 事件:

using(var enumerator = ((IEnumerable<Feature>)features).GetEnumerator())
{ 
  AnonymousClass x = new AnonymousClass();

  while(enumerator.MoveNext())
  {
     x.feature = (Feature)enumerator.Current;
     sscce.Click += new EventHandler(x.AnonymousMethod);
  }
}

如您所见,您已多次订阅同一个 AnonymousClass 实例的 AnonymousMethod。并且该实例将具有等于最后分配的功能的功能。现在,当您将当前功能复制到局部变量时会发生什么变化:

using(var enumerator = ((IEnumerable<Feature>)features).GetEnumerator())
{
  while(enumerator.MoveNext())
  {
     AnonymousClass x = new AnonymousClass();
     x.current = (Feature)enumerator.Current; // field has local variable name
     sscce.Click += new EventHandler(x.AnonymousMethod);
  }
}

在这种情况下,每次迭代都会创建 AnonymousClass 实例,因此不同类实例(每个都捕获自己的特征)的 AnonymousMethods 将处理 Click 事件。为什么代码不同 - 因为正如 Eric 所说,闭包(即匿名类)对变量是封闭的。为了封闭循环体中的局部变量,在第二种情况下,应在循环内创建匿名类的实例。

【讨论】:

  • @GrantWinney 是的,已经在添加该链接 :)
  • 拜托,您能否添加一个指向 Microsoft 版本说明的链接,说明此错误已修复?
  • @Aerospace 是的,这在 C# 5.0 规范的 8.8.4 foreach 语句 部分中有描述
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-08
  • 2020-11-11
  • 1970-01-01
  • 1970-01-01
  • 2010-10-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多