Какие лучшие практики для чистки ссылок обработчика событий?

Часто я нахожу меня написанием кода как это:

        if (Session != null)
        {
            Session.KillAllProcesses();
            Session.AllUnitsReady -= Session_AllUnitsReady;
            Session.AllUnitsResultsPublished -= Session_AllUnitsResultsPublished;
            Session.UnitFailed -= Session_UnitFailed;
            Session.SomeUnitsFailed -= Session_SomeUnitsFailed;
            Session.UnitCheckedIn -= Session_UnitCheckedIn;
            UnattachListeners();
        }

Причем цель состоит в том, чтобы очистить все подписки события, для которых мы зарегистрировались на цели (Сессия) так, чтобы Сессия была свободна быть расположенной GC. У меня было обсуждение с коллегой о классах, которые реализуют IDisposable однако, и это была его вера, что те классы должны формовать очистку как это:

    /// <summary>
    /// Disposes the object
    /// </summary>
    public void Dispose()
    {
        SubmitRequested = null; //frees all references to the SubmitRequested Event
    }

Существует ли причина того, чтобы предпочесть один по другому? Существует ли лучший способ пойти об этом в целом? (Кроме событий слабой ссылки везде)

То, что я действительно хотел бы видеть, является чем-то сродни безопасному шаблону вызова для того, чтобы сгенерировать события: т.е. безопасный и повторяемый. Что-то, что я могу не забыть делать каждый раз, я присоединяю к событию так, чтобы я мог удостовериться, что для меня будет легко вымыться.

32
задан Firoso 15 July 2010 в 17:38
поделиться

2 ответа

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

--------------      ------------      ----------------
|            |      |          |      |              |
|Event Source|  ==> | Delegate |  ==> | Event Target |
|            |      |          |      |              |
--------------      ------------      ----------------

Итак, в вашем случае источником события является объект Session . Но я не вижу, чтобы вы упомянули, какой класс объявил обработчики, поэтому мы еще не знаем, кто является целью события. Давайте рассмотрим две возможности. Целью события может быть тот же объект Session , который представляет источник, или это может быть совершенно отдельный класс. В любом случае и при нормальных обстоятельствах сеанс будет собираться до тех пор, пока нет другой ссылки, даже если обработчики его событий остаются зарегистрированными. Это потому, что делегат не содержит ссылки на источник события. Он содержит только ссылку на цель события.

Рассмотрим следующий код.

public static void Main()
{
  var test1 = new Source();
  test1.Event += (sender, args) => { Console.WriteLine("Hello World"); };
  test1 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();

  var test2 = new Source();
  test2.Event += test2.Handler;
  test2 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Source()
{
  public event EventHandler Event;

  ~Source() { Console.WriteLine("disposed"); }

  public void Handler(object sender, EventArgs args) { }
}

Вы увидите, что «disposed» дважды выводится на консоль, подтверждая, что оба экземпляра были собраны, без отмены регистрации события. Причина, по которой объект, на который ссылается test2 , собирается, заключается в том, что он остается изолированным объектом в ссылочном графе (как только test2 имеет значение null, то есть), даже если он имеет обратную ссылку на сам хоть событие.

Сложность усложняется, когда вы хотите, чтобы у цели события было время жизни, которое короче, чем у источника события. В этом случае вы должны отменить регистрацию событий.Рассмотрим следующий код, демонстрирующий это.

public static void Main()
{
  var parent = new Parent();
  parent.CreateChild();
  parent.DestroyChild();
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Child
{
  public Child(Parent parent)
  {
    parent.Event += this.Handler;
  }

  private void Handler(object sender, EventArgs args) { }

  ~Child() { Console.WriteLine("disposed"); }
}

public class Parent
{
  public event EventHandler Event;

  private Child m_Child;

  public void CreateChild()
  {
    m_Child = new Child(this);
  }

  public void DestroyChild()
  {
    m_Child = null;
  }
}

Вы увидите, что «disposed» никогда не выводится на консоль, демонстрируя возможную утечку памяти. Это особенно трудная проблема. Внедрение IDisposable в Child не решит проблему, потому что нет гарантии, что вызывающие абоненты будут правильно играть и фактически вызовут Dispose .

Ответ

Если ваш источник событий реализует IDisposable , то вы на самом деле не купили себе ничего нового. Это связано с тем, что, если источник события больше не является корневым, то цель события также не будет иметь root-права.

Если ваша цель события реализует IDisposable , то она может очистить себя от источника события, но нет гарантии, что Dispose будет вызван.

Я не говорю, что отмена регистрации событий из Dispose неверна. Я хочу сказать, что вам действительно нужно изучить, как определяется иерархия классов, и подумать, как лучше всего избежать проблемы с утечкой памяти, если она вообще существует.

43
ответ дан 27 November 2019 в 20:42
поделиться

Реализация IDisposable имеет два преимущества перед ручным методом:

  1. Это стандарт, и компилятор относится к нему особым образом. Это означает, что все, кто читает ваш код, понимают, о чем идет речь, как только видят реализацию IDisposable.
  2. .NET C# и VB предоставляют специальные конструкции для работы с IDisposable через оператор using.

Тем не менее, я сомневаюсь, что это полезно в вашем сценарии. Чтобы безопасно утилизировать объект, он должен быть утилизирован в finally-блоке внутри try/catch. В случае, который вы описываете, может потребоваться, чтобы либо Session позаботился об этом, либо код, вызывающий Session, при удалении объекта (т.е. в конце его области видимости: в блоке finally). Если это так, то сессия должна реализовать и IDisposable, что соответствует общей концепции. Внутри метода IDisposable.Dispose он перебирает все свои члены, которые являются одноразовыми, и избавляется от них.

Редактировать

Ваш последний комментарий заставляет меня переосмыслить мой ответ и попытаться соединить несколько точек. Вы хотите убедиться, что Session утилизируется GC. Если ссылки на делегатов находятся внутри одного класса, то отписывать их совсем не обязательно. Если они из другого класса, то их нужно отписать. Глядя на приведенный выше код, кажется, что вы должны написать этот блок кода в любом классе, который использует Session, и очистить его в какой-то момент процесса.

Если Session нужно освободить, есть более прямой способ - вызывающий класс не должен отвечать за правильную обработку процесса отписки. Просто переберите все события, используя тривиальное отражение, и установите все в null (вы можете рассмотреть альтернативные подходы для достижения того же эффекта).

Поскольку вы спрашиваете о "лучших практиках", вам следует объединить этот метод с IDisposable и реализовать цикл внутри IDisposable.Dispose(). Перед тем как войти в этот цикл, вызывается еще одно событие: Disposing, которое слушатели могут использовать, если им нужно что-то убрать самостоятельно. При использовании IDisposable следует помнить о его недостатках, из которых этот кратко описанный паттерн является обычным решением.

5
ответ дан 27 November 2019 в 20:42
поделиться
Другие вопросы по тегам:

Похожие вопросы: