Частное поле, снятое анонимным делегатом

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, я беспокоюсь, чтобы это вызвало утечку памяти.

Ответ 1

Это проще всего понять, посмотрев код, сгенерированный компилятором, который похож на:

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 - это метод-экземпляр, ваша "проблема" (если вы хотите, чтобы об этом думали так) только что ухудшилась. Теперь компилятор должен создать event-listener, который "запоминает" как локальный, так и содержащий экземпляр объекта 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);
    }
}

Ответ 2

Ответ Ani правильный. Подведение итогов и добавление некоторых деталей:

Поскольку делегат захватывает переменную this._bar, она неявно удерживается в экземпляре B?

Да. "this".

Будет ли ссылаться на экземпляр 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

Мы надеемся исправить это в гипотетической будущей версии С#; мы могли бы лучше разделить блокировки вместо того, чтобы создать одно большое закрытие. Однако это не произойдет в ближайшее время.

Кроме того, функция "async/await" на С# 5 также, вероятно, усугубит ситуации, когда локальные жители будут жить дольше, чем вы ожидали. Никто из нас не в восторге от этого, но, как говорится, совершенным является враг удивительного. У нас есть некоторые идеи о том, как мы можем настроить codegen асинхронных блоков, чтобы улучшить ситуацию, но не promises.

Ответ 3

Если вы добавляете анонимный метод к событию и хотите его уважать, вам нужно будет установить событие в null или сохранить делегата в списке, чтобы впоследствии вы могли "- =" его от вашего события.

Но да, вы можете получить "утечку памяти" из объектов, на которые ссылается делегат, прикрепленный к событию.