Как события вызывают утечки памяти в C# и как слабые ссылки помогают смягчить это?

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

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

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

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

3 ответов


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

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

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

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

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

читать Джон Скит отлично статьи на события. Это не настоящая "утечка памяти" в классическом смысле, а скорее удерживаемая ссылка, которая не была отключена. Так что всегда помните -= обработчик событий, который вы += в предыдущей точке, и вы должны быть золотыми.


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

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

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

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