【问题标题】:Private field captured in anonymous delegate匿名委托中捕获的私有字段
【发布时间】:2012-01-15 02:11:45
【问题描述】:
class A
{
   public event EventHandler AEvent;
}
class B
{
   private A _foo;
   private int _bar;

   public void AttachToAEvent()
   {
      _foo.AEvent += delegate()
      {
         ...
         UseBar(_bar);
         ...
      }
   }
} 

既然delegate 捕获了变量this._bar,它是否隐含地持有B 的实例? B 的实例是否会通过事件处理程序引用并被A 的实例捕获变量?

如果_barAttachToAEvent 方法的局部变量,会有什么不同吗?

因为在我的例子中,A 的实例比B 的实例寿命更长,而且比B 的实例小得多,我担心这样做会导致“内存泄漏”。

【问题讨论】:

  • 属性和任何字段都存在同样的问题。我倾向于不捕获其他可访问的成员。安全起见,并分配给当地人。这样一来,您现在在方法的动态范围之外几乎是不可变的。

标签: c# memory-leaks closures anonymous-methods object-lifetime


【解决方案1】:

如果您将匿名方法添加到事件并希望延迟它,则必须将事件设置为 null 或将您的委托存储在列表中,以便以后可以从您的事件中“-=”它。

但是,您可以从附加到事件的委托中引用的对象获得“内存泄漏”。

【讨论】:

  • 处理程序应该保持连接,只要持有它的 A 的实例还活着,_bar 变量也应该如此。不应该附加它的 B 实例。会吗?
  • 循环引用呢?似乎是这样。
【解决方案2】:

这点看编译器生成的代码最容易理解,类似于:

public void AttachToAEvent()
{
    _foo.AEvent += new EventHandler(this.Handler);
}

[CompilerGenerated]
private void Handler(object sender, EventArgs e)
{
    this.UseBar(this._bar);
}

可以清楚地看到,创建的委托是一个instance-delegate(以对象上的实例方法为目标),因此必须持有对该对象实例的引用。

由于委托捕获变量 this._bar,它是否隐含地持有 B 的实例?

实际上,匿名方法只捕获this(而不是this._bar)。从生成的代码可以看出,构造的委托确实会持有对B 实例的引用。它必须;每当执行委托时,如何按需读取该字段?请记住,变量被捕获,而不是

因为在我的例子中,A 的实例寿命要长得多,而且要小得多 比 B 的实例,我担心这样做会导致“内存泄漏” 这个。

是的,你完全有理由这样做。只要A 实例可以访问,B 事件订阅者仍然可以访问。如果您不想花哨的弱事件,则需要重写它,以便在不再需要处理程序时取消注册。

如果 _bar 是 AttachToAEvent 方法?

是的,它会,因为捕获的变量会变成 bar 本地变量,而不是 this。 但是假设UseBar 是一个实例方法,你的“问题”(如果你想这样想的话)只会变得更糟。编译器现在需要生成一个“记住”本地和包含B 对象实例的事件侦听器。

这是通过创建一个闭包对象并使其(实际上是它的一个实例方法)成为委托的目标来实现的。

public void AttachToAEvent(int _bar)
{
    Closure closure = new Closure();
    closure._bar = _bar;
    closure._bInstance = this;
    _foo.AEvent += new EventHandler(closure.Handler);
}

[CompilerGenerated]
private sealed class Closure
{
    public int _bar;
    public B _bInstance;

    public void Handler(object sender , EventArgs e)
    {
        _bInstance.UseBar(this._bar);
    }
}

【讨论】:

  • 问题并不总是变得更糟,不是吗?如果 this 从未被引用(这意味着也没有调用任何实例方法),它就不会成为闭包的一部分,对吗?
  • @configurator:如果UseBar是实例方法,this肯定会被闭包捕获。如果不是,则编译器没有理由实际捕获this。但是发生了一些有趣的事情(很可能是编译器错误):stackoverflow.com/questions/8419079/…
【解决方案3】:

阿尼的回答是正确的。总结和补充一些细节:

由于委托捕获变量 this._bar,它是否隐含地持有 B 的实例?

是的。 “this”被捕获。

B的实例会被事件处理程序引用并被A的实例捕获变量吗?

是的。

如果 _bar 是 AttachToAEvent 方法的局部变量,会有什么不同吗?

是的。在这种情况下,闭包对象将保留本地; local 将被实现为闭包的一个字段。

因为在我的例子中,A 的实例比 B 的实例寿命长得多,而且比 B 的实例小得多,所以我担心这样做会导致“内存泄漏”。

您的担心是完全正确的。你的情况已经很糟糕了,但事实上,当你有 两个 匿名函数在运行时,情况可能会相当糟糕。现在,同一局部变量声明空间中的所有匿名函数共享一个公共闭包,这意味着 all 封闭的外部变量(包括“this”)的生命周期延长至 他们中寿命最长的。有关详细信息,请参阅我关于该主题的文章:

http://blogs.msdn.com/b/ericlippert/archive/2007/06/06/fyi-c-and-vb-closures-are-per-scope.aspx

我们希望在假设的 C# 未来版本中解决此问题;我们可以更好地划分闭包,而不是创建一个大闭包。然而,这不会很快发生。

此外,C# 5 的“async/await”特性也可能会加剧当地人最终活得比您预期更长的情况。我们没有人对此感到兴奋,但正如他们所说,完美是令人敬畏的敌人。我们对如何调整异步块的代码生成以改善这种情况有一些想法,但没有承诺。

【讨论】:

  • +1 表示“完美是令人敬畏的敌人” - 任何努力的好建议。
猜你喜欢
  • 2010-11-01
  • 2011-05-25
  • 2010-11-01
  • 2014-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多