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