Какие рекомендации по очистке ссылок на обработчик событий?
Часто я нахожу, что пишу код следующим образом:
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
}
Есть ли причина для предпочтения одного над другим? Есть ли лучший способ это сделать? (Помимо слабых опорных событий всюду)
То, что я действительно хотел бы видеть, похоже на шаблон безопасного вызова для поднятия событий: безопасный и повторяемый. Что-то, что я могу запомнить каждый раз, когда я прикрепляю к событию, чтобы я мог убедиться, что мне будет легко очистить.
Ответы
Ответ 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.