Нужно ли выполнять отписку от события, содержащемся в объекте, который будет собран GC?
Доброго времени суток, появился небольшой вопрос.
Имеется класс, например некий Task. Он содержит в себе событие TaskComplete, на которое кто-нибудь подписался. И есть некий класс Ship, содержащий свойство CurrentTask типа Task (ну или поле, не суть важно). Предположим, задача выполнена и CurrentTask присваивается другой экземпляр Task или null. Логично, что старый экземпляр будет через некоторое время собран GC.
Вроде бы очевидно, что собрано будет все и никаких проблем от того, что на то событие был кто-то подписан не будет (в отличие от обратной операции, когда удаляется подписчик, естественно). Но на всякий случай уточню - правильно ли я понимаю, что выполнять отписку от события, которое скоро будет собрано GC, нет смысла и это не приведет к memory leak?
Я попытался смоделировать вашу ситуацию и получилось, что не отписка не приводит к утечке памяти. Я использовал финализатор, чтобы отследить сборку мусора. В конце теста примерно 14% объектов не уничтожено, но вызов GC.Collect() приводит к их уничтожению. Я это интерпретирую так, что GC распознает эти объекты как мусор, просто еще не добрался до них. Это связано и с использованием кастомного финализатора: для уничтожения таких объектов сборщику мусора нужно больше времени. Подробнее про события https://habrahabr.ru/post/89529/
class Program
{
private static bool Unsubscribe = false;
static void Main(string[] args)
{
var ship = new Ship();
Enumerable.Range(0, 100000).ToList().ForEach(i =>
{
Console.Write($"\r{i + 1}\t");
ship.MyTask = new MyTask(i);
ship.MyTask.TaskCompleted += OnTaskCompleted;
ship.MyTask.DoAndRaise();
});
ship.MyTask = null;
Console.WriteLine();
Console.WriteLine("Raised: " + MyTask.EventsRaised);
Console.WriteLine("Collected: " + MyTask.Collected);
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Collected: " + MyTask.Collected);
}
static void OnTaskCompleted(object semder, int taskId)
{
if (Unsubscribe)
{
var mt = semder as MyTask;
mt.TaskCompleted -= OnTaskCompleted;
}
}
}
public class MyTask
{
public static int Collected, EventsRaised;
public MyTask(int id)
{
TaskId = id;
}
public int TaskId { get; }
public event EventHandler<int> TaskCompleted;
public void DoAndRaise()
{
TaskCompleted?.Invoke(this, TaskId);
Interlocked.Increment(ref EventsRaised);
}
~MyTask()
{
Interlocked.Increment(ref Collected);
}
}
public class Ship
{
public MyTask MyTask { get; set; }
}
.. The tricky case is when the publisher is long-lived but the subscribers don't want to be - in that case you need to unsubscribe the handlers..
Если источник события скоро будет собран, на него нет других ссылок, и на подписчика ссылается только источник, то не обязательно отписываться.
Я представляю себе это как от дерева отпала ветка на которой есть другие веточки с листьями, внутри они сами на себя могут ссылаться сколь угодно, но т.к. нет ссылки на само дерево их породившее, то такая ветка будет собрана GC.