Подтвердить что ты не робот

Какие рекомендации по очистке ссылок на обработчик событий?

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

        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
    }

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

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

4b9b3361

Ответ 1

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

--------------      ------------      ----------------
|            |      |          |      |              |
|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) { }
}

Вы увидите, что "удаленный" дважды печатается на консоли, проверяя, что оба экземпляра были собраны, не отменив регистрацию события. Причина, по которой объект, на который ссылается 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;
  }
}

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

Ответ

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

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

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

Ответ 2

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

  • Он стандартный, и компилятор рассматривает его специально. Это означает, что все, кто читает ваш код, понимают, что это за минуту, когда они видят, что IDisposable реализуется.
  • .NET С# и VB предоставляют специальные конструкции для работы с IDisposable через оператор using.

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

Изменить

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

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

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

Ответ 3

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

Ответ 4

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

например.

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

Это статья, которая обеспечивает дополнительную обратную связь для других применений для свойства ADD REMOVE: http://msdn.microsoft.com/en-us/library/8843a9ch.aspx

Ответ 5

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

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

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

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