Какие утечки памяти вызывают обработчики лямбда-событий? - PullRequest
0 голосов
/ 17 января 2019

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

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

public class SomeClass
{
    public event EventHandler SomeEvent;
}
public class MyClass
{
    public MyClass(SomeClass source)
    {
        //VERSION 1
        source.SomeEvent += OnSomeEvent;

        //VERSION 2
        void localHandler(object s, EventArgs args) { Console.WriteLine("some action with(out) any references"); }
        source.SomeEvent += localHandler;

        //VERSION 3
        var x = new object();
        source.SomeEvent += (s, e) => { Console.WriteLine("some event fired, using closure x:" + x.GetHashCode()); };

        //VERSION 4
        source.SomeEvent += (s, e) => { Console.WriteLine("some action without any references"); };
    }

    private void OnSomeEvent(object sender, EventArgs e) 
    {
        //...
    }
}

Мои предположения / вопросы, почему различные версии обработки событий могут вызывать утечку памяти:

  • Версия 1: поскольку цель вызова явно ссылается на экземпляр MyClass.
  • Версия 2: поскольку ссылка на localHandler подразумевает ссылку на экземпляр MyClass - за исключением, может быть, если код внутри localHandler не имеет ссылок на экземпляр MyClass?
  • Версия 3: поскольку лямбда содержит замыкание, которое само по себе является ссылкой на экземпляр MyClass - или это так?
  • Версия 4: поскольку лямбда не ссылается на экземпляр MyClass, это не может вызвать утечку?

И дополнительные вопросы к версиям 3 и 4:

  • Где хранится "магический вспомогательный объект", который .Net создает для лямбды / замыкания, и содержит ли он (всегда) ссылку, которая поддержит экземпляр MyClass в живых?
  • Если обработчики лямбда-событий могут просочиться, их следует использовать только в тех случаях, когда это не является проблемой (например, MyClass экземпляры переживают SomeClass экземпляров), поскольку их нельзя удалить с помощью -=?

РЕДАКТИРОВАТЬ: Этот пост (с оригинальным названием "Когда обработчики событий могут вызвать утечки памяти?") Был предложен как дубликат, из Почему и Как избежать утечек памяти обработчика событий? , но я не согласен, потому что вопрос был направлен именно на обработчики лямбда-событий. Я перефразировал вопрос / название, чтобы сделать это более ясным.

1 Ответ

0 голосов
/ 17 января 2019

Отказ от ответственности : Я не могу гарантировать, что это 100% правда - ваш вопрос достаточно глубокий, и я могу ошибиться.

Тем не менее, я надеюсь, что это даст вам некоторые мысли или указания.

Давайте рассмотрим этот вопрос согласно CLR организации памяти:

Локальные переменные метода и параметры метода хранятся в кадре стека методов в памяти (кроме случаев, когда они объявлены с ключевым словом ref).

Стек хранит типы значений и ссылки на переменные ссылочного типа, которые указывают на объекты в куче.

Фрейм стека метода существует во время выполнения метода, а локальные переменные метода исчезнут вместе с фреймом стека после завершения метода.

За исключением случаев, когда локальные переменные были захвачены так или иначе, это также относится к работе компилятора , вы можете прочитать об этом на сайте Джона Скита:

http://jonskeet.uk/csharp/csharp2/delegates.html#captured.variables

Версия 1 : OnSomeEvent метод является членом MyClass и будет перехвачен экземпляром Someclass source, пока делегаты, ссылающиеся на этот метод, не будут удалены из события. Таким образом, экземпляр MyClass, созданный в конструкторе, помещенный в кучу и содержащий этот метод, не будет собираться GC, пока ссылка на его метод не будет удалена из события.

Компилятор компилирует лямбду особым образом, прочитайте Пример реализации абзац полностью:

https://github.com/dotnet/csharplang/blob/master/spec/conversions.md#anonymous-function-conversions

Версия 4 : 2 ссылки, которые я дал, лямбда будет скомпилирована в метод MyClass, который будет перехвачен экземпляром SomeClass, как в Версия 1

Версия 2 : Я не знаю нюансов о том, как будут компилироваться локальные методы, но он должен быть таким же, как в Версия 4 (и, следовательно, Версия 1 ).

Версия 3 : Все локальные переменные будут захвачены интересным способом.

У вас также есть «объект x», поэтому будет создан сгенерированный компилятором класс, который будет содержать открытое поле public object x; и метод, который будет переведен из вашей лямбды (см. Пример реализации параграф).

Итак, я думаю, что в версии 1,2,4 внутренне будут одинаковыми: MyClass будет содержать метод, который будет использоваться в качестве обработчика событий.

В версии 3 будет создан сгенерированный компилятором класс, в котором будут храниться ваша локальная переменная и метод, переведенные из lamdba.

Любой экземпляр любого класса не будет собираться GC до тех пор, пока у SomeClass не появится метод в списке вызовов.

...