匿名委托中捕获的私有字段

12
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 的实例?A 的一个实例会通过事件处理程序和被 A 的一个实例捕获的变量来引用 B 的实例吗?

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

在我的情况中,A 的一个实例比 B 的一个实例寿命长得多且更小,我担心这样做会导致"内存泄漏"。


同样的问题也适用于属性和字段。我倾向于不捕获其他可访问成员。采取安全措施,分配给本地变量。这样你就知道它在方法的动态范围之外几乎是不可变的。 - leppie
3个回答

15

Ani的回答是正确的。总结并添加了一些细节:

由于委托捕获变量this._bar,它是否隐式地保留了B类的实例?

是的,“this”被捕获。

通过事件处理程序和捕获变量,A类的实例会引用B类的实例吗?

是的。

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

会。在这种情况下,闭包对象将持有该局部变量;该局部变量将作为闭包的字段实现。

由于在我的情况下,A类的实例生存时间比B类的实例长得多且小得多,因此我担心通过这样做会导致“内存泄漏”。

你完全正确担心。你的情况已经很糟糕了,但事实上,当涉及到两个匿名函数时,情况可能会更加糟糕。目前,在同一局部变量声明空间中的所有匿名函数共享一个公共闭包,这意味着所有封闭的外部变量(包括“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
“完美是卓越的敌人”这句话对于任何事业都是一个好建议。+1 - Jesse C. Slicer

10

这最好通过查看编译器生成的代码来理解,它类似于:

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

[CompilerGenerated]
private void Handler(object sender, EventArgs e)
{
    this.UseBar(this._bar);
}
如显然所示,创建的委托是一个实例委托(针对对象上的实例方法),因此必须持有对该对象实例的引用。
“由于委托捕获了变量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
@configurator:如果UseBar是一个实例方法,那么this一定会被闭包捕获。如果不是,编译器就没有理由去捕获this。但是有些奇怪的事情正在发生(很可能是编译器的一个bug):https://dev59.com/tmoy5IYBdhLWcg3wo_gn - Ani

0
如果您向事件添加匿名方法并希望取消引用它,您将不得不将事件设置为null或将委托存储在列表中,以便稍后从事件中“-=”它。
但是,是的,您可以从附加到事件的委托中引用的对象中获得“内存泄漏”。

处理程序应该保持附加状态,只要持有它的 A 实例仍然存在,_bar 变量也应该如此。附加它的 B 实例不应该这样做。难道会吗? - Damir
循环引用怎么办?这似乎是这种情况。 - Luca

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接