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

Как события вызывают утечку памяти на С# и как облегчить работу слабых ссылок?

Есть два способа (что я знаю), чтобы вызвать непреднамеренную утечку памяти в С#:

  • Не утилизировать ресурсы, которые реализуют IDisposable
  • Неправильно ссылаются и де-ссылки на события.

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

Можете ли вы объяснить, как события могут вызвать утечки памяти с кодом на С# и как я могу кодировать, чтобы обойти его с помощью слабых ссылок и без слабых ссылок?

4b9b3361

Ответ 1

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

Рассмотрим следующие классы:

class Source
{
    public event EventHandler SomeEvent;
}

class Listener
{
    public Listener(Source source)
    {
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        source.SomeEvent += new EventHandler(source_SomeEvent);
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }
}

... и затем следующий код:

Source newSource = new Source();
Listener listener = new Listener(newSource);
listener = null;

Даже если мы назначим null на listener, он не будет иметь права на сбор мусора, так как newSource все еще держит ссылку на обработчик события (Listener.source_SomeEvent). Чтобы устранить эту утечку, важно всегда отключать прослушиватели событий, когда они больше не нужны.

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

class Listener
{
    private Source _source;
    public Listener(Source source)
    {
        _source = source;
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        _source.SomeEvent += source_SomeEvent;
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }

    public void Close()
    {
        if (_source != null)
        {
            // detach event handler
            _source.SomeEvent -= source_SomeEvent;
            _source = null;
        }
    }
}

Затем вызывающий код может сигнализировать, что он выполняется с помощью объекта, который удалит ссылку, которая Source имеет значение "Listener";

Source newSource = new Source();
Listener listener = new Listener(newSource);
// use listener
listener.Close();
listener = null;

Ответ 2

Прочитайте Jon Skeet отлично article о событиях. Это не настоящая "утечка памяти" в классическом смысле, а более содержательная ссылка, которая не была отключена. Поэтому всегда помните -= обработчик событий, что вы += в предыдущей точке, и вы должны быть золотыми.

Ответ 3

В "песочнице" управляемого проекта .NET нет "утечек памяти"; есть только ссылки, проведенные дольше, чем разработчик счел бы необходимым. Фредрик имеет право на это; когда вы прикрепляете обработчик к событию, потому что обработчик обычно является методом экземпляра (требующим экземпляра), экземпляр класса, содержащего слушатель, остается в памяти, пока эта ссылка поддерживается. Если экземпляр прослушивателя поочередно содержит ссылки на другие классы (например, обратные ссылки на содержащие объекты), куча может оставаться довольно большой после того, как слушатель вышел из всех других областей.

Возможно, кто-то с немного более эзотерическим знанием делегата и MulticastDelegate может пролить свет на это. Как я вижу это, истинная утечка МОЖЕТ быть возможной, если бы все следующие были правдой:

  • Слушателю событий требуется, чтобы внешние/неуправляемые ресурсы были освобождены путем реализации IDisposable, но он либо не делает, либо
  • Делегат многоадресной передачи событий НЕ вызывает методы Dispose() из своего переопределенного метода Finalize() и
  • Класс, содержащий событие, не вызывает Dispose() для каждого целевого объекта делегата через его собственную реализацию IDisposable или в Finalize().

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