Какие рекомендации по очистке ссылок на обработчики событий? - PullRequest
31 голосов
/ 15 июля 2010

Часто я нахожу, что пишу код, подобный следующему:

        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
    }

Есть ли причина для предпочтения одного над другим?Есть ли лучший способ сделать это вообще?(Помимо слабых ссылочных событий повсюду)

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

Ответы [ 6 ]

41 голосов
/ 15 июля 2010

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

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

Таким образом, в вашем случае источником события является Session объект. Но я не вижу, чтобы вы упомянули, какой класс объявил обработчики, поэтому мы еще не знаем, кто является целью события. Давайте рассмотрим две возможности. Целью события может быть тот же самый объект 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 установлено нулевое значение), даже несмотря на то, что он имеет ссылку на самого себя через событие.

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

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, значит, вы действительно не купили себе ничего нового. Это происходит потому, что если источник события больше не укоренен, чем цель события больше не будет также укореняться.

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

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

4 голосов
/ 15 июля 2010

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

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

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

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

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

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

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

3 голосов
/ 30 июня 2012

Шаблон обработки событий, автоматически генерируемый с помощью ключевого слова vb.net WithEvents, довольно приличный.Код VB (примерно):

WithEvents myPort As SerialPort

Sub GotData(Sender As Object, e as DataReceivedEventArgs) Handles myPort.DataReceived
Sub SawPinChange(Sender As Object, e as PinChangedEventArgs) Handles myPort.PinChanged

будет переведен в эквивалент:

SerialPort _myPort;
SerialPort myPort
{  get { return _myPort; }
   set {
      if (_myPort != null)
      {
        _myPort.DataReceived -= GotData;
        _myPort.PinChanged -= SawPinChange;
      }
      _myPort = value;
      if (_myPort != null)
      {
        _myPort.DataReceived += GotData;
        _myPort.PinChanged += SawPinChange;
      }
   }
}

Это разумный пример, которому нужно следовать;если вы используете этот шаблон, то в Dispose вы установите null все свойства, связанные с событиями, что, в свою очередь, позаботится об их отмене.

Если кто-то хочет немного автоматизировать удаление, такчтобы гарантировать, что все будет удалено, можно изменить это свойство так:

Action<myType> myCleanups; // Just once for the whole class
SerialPort _myPort;
static void cancel_myPort(myType x) {x.myPort = null;}
SerialPort myPort
{  get { return _myPort; }
   set {
      if (_myPort != null)
      {
        _myPort.DataReceived -= GotData;
        _myPort.PinChanged -= SawPinChange;
        myCleanups -= cancel_myPort;
      }
      _myPort = value;
      if (_myPort != null)
      {
        myCleanups += cancel_myPort;
        _myPort.DataReceived += GotData;
        _myPort.PinChanged += SawPinChange;
      }
   }
}
// Later on, in Dispose...
  myCleanups(this);  // Perform enqueued cleanups

Обратите внимание, что перехват статических делегатов для myCleanups означает, что даже если существует много экземпляров myClass,будет только одна копия каждого делегата в масштабе всей системы.Возможно, это не имеет большого значения для классов с несколькими экземплярами, но потенциально важно, если класс будет создаваться много тысяч раз.

2 голосов
/ 06 августа 2012

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

Disposable.Create(() => {
                this.ViewModel.Selection.CollectionChanged -= SelectionChanged;
            })

Если вы затем сохраните это в каком-либо объекте GroupDisposable, который будет привязан к правильной области, то все готово.

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

1 голос
/ 30 июня 2012

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

Например,

#region Control Event Clean up
private event NotifyCollectionChangedEventHandler CollectionChangedFiles
{
    add { FC.CollectionChanged += value; }
    remove { FC.CollectionChanged -= value; }
}
#endregion Control Event Clean up

Эта статья предоставляет дополнительную обратную связь для других применений свойства.ДОБАВИТЬ УДАЛИТЬ: http://msdn.microsoft.com/en-us/library/8843a9ch.aspx

0 голосов
/ 26 октября 2018

Ответ от DanH почти готов, но в нем отсутствует один важный элемент.

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

List<IDisposable> eventsToDispose = new List<IDisposable>();

var handlerCopy = this.ViewModel.Selection;
eventsToDispose.Add(Disposable.Create(() => 
{
    handlerCopy.CollectionChanged -= SelectionChanged;
}));

И позже мы можем распоряжаться всеми событиями, используя это:

foreach(var d in eventsToDispose)
{ 
    d.Dispose();
}

Если мы хотим сделать его короче:

eventsToDispose.ForEach(o => o.Dispose());

Если мы хотим сделать его еще короче, мы можем заменить IList на CompositeDisposable, что точно так же за кадром.

Тогда мы можем распоряжаться всеми событиями следующим образом:

eventsToDispose.Dispose();
...