C# 在对集合进行迭代时订阅闭包中的事件将导致订阅该事件的最后一项

C# 在对集合进行迭代时订阅闭包中的事件将导致订阅该事件的最后一项,c#,linq,foreach,C#,Linq,Foreach,守则: 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 { } cl

守则:

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
关键字允许
Feature
值在lambda的动作主体中使用,在
.doIt()之前


C#中的好语法是什么?

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

 foreach (Feature feature in features) {
    Feature current = feature;
    sscce.Click += (object sender, EventArgs e) => { current.doIt(); };
 }
我建议你们阅读埃里克·利珀特博客上的文章

注意:这在C#的上一个版本中是固定的


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

private sealed class AnonymousClass
{
    public Feature feature;

    public void AnonymousMethod(object sender, EventArgs e)
    {
        this.feature.doIt();
    }
}
您的代码将被修改,以便它使用此AnonymousClass的实例并订阅其AnonymousMethod以单击事件:

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);
  }
}
使用(var枚举器=((IEnumerable)功能)。GetEnumerator()
{ 
AnonymousClass x=新的AnonymousClass();
while(枚举数.MoveNext())
{
x、 feature=(feature)枚举器.Current;
sscce.Click+=neweventhandler(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);
  }
}
使用(var枚举器=((IEnumerable)功能)。GetEnumerator()
{
while(枚举数.MoveNext())
{
AnonymousClass x=新的AnonymousClass();
x、 current=(功能)enumerator.current;//字段具有局部变量名
sscce.Click+=neweventhandler(x.AnonymousMethod);
}
}

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

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

 foreach (Feature feature in features) {
    Feature current = feature;
    sscce.Click += (object sender, EventArgs e) => { current.doIt(); };
 }
我建议你们阅读埃里克·利珀特博客上的文章

注意:这在C#的上一个版本中是固定的


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

private sealed class AnonymousClass
{
    public Feature feature;

    public void AnonymousMethod(object sender, EventArgs e)
    {
        this.feature.doIt();
    }
}
您的代码将被修改,以便它使用此AnonymousClass的实例并订阅其AnonymousMethod以单击事件:

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);
  }
}
使用(var枚举器=((IEnumerable)功能)。GetEnumerator()
{ 
AnonymousClass x=新的AnonymousClass();
while(枚举数.MoveNext())
{
x、 feature=(feature)枚举器.Current;
sscce.Click+=neweventhandler(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);
  }
}
使用(var枚举器=((IEnumerable)功能)。GetEnumerator()
{
while(枚举数.MoveNext())
{
AnonymousClass x=新的AnonymousClass();
x、 current=(功能)enumerator.current;//字段具有局部变量名
sscce.Click+=neweventhandler(x.AnonymousMethod);
}
}

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

您使用的是什么版本的C?我使用的是Microsoft Visual C 2010,编译器在哪里?它叫什么名字?Jon Skeet有一个有用的页面。长话短说,您可能正在使用C#4.0或更早版本:此修复程序直到C#5才推出。您使用的是什么版本的C#?我使用的是Microsoft Visual C#2010,编译器在哪里?它叫什么名字?Jon Skeet有一个有用的页面。长话短说,您可能正在使用C#4.0或更早版本:直到C#5才引入此修复。@Grantwiney yep,已经添加了该链接:)请您添加一个指向Microsoft版本说明的链接,解释此错误已修复吗?@Aerospace yes,第8.8.4节C#5.0的foreach声明对此进行了描述specification@GrantWinney是的,已经添加了该链接:)请,您可以添加一个指向Microsoft版本说明的链接,说明此错误已修复吗?@Aerospace是的,这在C#5.0规范的foreach声明第8.8.4节中进行了描述