Закрытое поле захвачено анонимным делегатом
class A
{
public event EventHandler AEvent;
}
class B
{
private A _foo;
private int _bar;
public void AttachToAEvent()
{
_foo.AEvent += delegate()
{
...
UseBar(_bar);
...
}
}
}
Так как делегат захватывает переменную this._bar, неявно ли он хранится в экземпляре B? Будет ли на экземпляр B ссылаться через обработчик событий и перехватывать переменную экземпляром A?
Было бы по-другому, если бы _bar был локальной переменной метода AttachToAEvent?
Так как в моем случае экземпляр A живет намного дольше и намного меньше, чем экземпляр B, я обеспокоен тем, что при этом произойдет "утечка памяти".
3 ответа
Это проще всего понять, посмотрев код, сгенерированный компилятором, который похож на:
public void AttachToAEvent()
{
_foo.AEvent += new EventHandler(this.Handler);
}
[CompilerGenerated]
private void Handler(object sender, EventArgs e)
{
this.UseBar(this._bar);
}
Как видно, созданный делегат является экземпляром-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._bar, неявно ли он относится к экземпляру B?
Да. "это" захвачено.
Будет ли ссылка на экземпляр B через обработчик событий и захваченную переменную экземпляром A?
Да.
Было бы по-другому, если бы _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#; мы могли бы лучше разделить замыкания вместо создания одного большого замыкания. Однако это не произойдет в ближайшее время.
Более того, функция "async/await" в C# 5 также, вероятно, усугубит ситуации, в которых местные жители будут жить дольше, чем вы ожидаете. Никто из нас не в восторге от этого, но, как говорится, совершенство - враг удивительного. У нас есть некоторые идеи о том, как мы можем настроить codegen асинхронных блоков, чтобы улучшить ситуацию, но никаких обещаний.
Если вы добавляете анонимный метод к событию и хотите отложить его, вам нужно будет установить для этого события значение null или сохранить свой делегат в списке, чтобы позже вы могли "-=" использовать его в своем событии.
Но да, вы можете получить "утечку памяти" из объектов, на которые есть ссылки в делегате, прикрепленном к событию.