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

Скажем, у нас есть следующий метод:

private MyObject foo = new MyObject();

// and later in the class

public void PotentialMemoryLeaker(){
  int firedCount = 0;
  foo.AnEvent += (o,e) => { firedCount++;Console.Write(firedCount);};
  foo.MethodThatFiresAnEvent();
}

Если создается экземпляр класса с этим методом и PotentialMemoryLeaker метод вызывается несколько раз, происходит ли утечка памяти?

Есть ли способ отцепить этот обработчик лямбда-событий после завершения вызова MethodThatFiresAnEvent ?

Ответов (5)

Решение

Да, сохраните его в переменной и отцепите.

DelegateType evt = (o, e) => { firedCount++; Console.Write(firedCount); };
foo.AnEvent += evt;
foo.MethodThatFiresAnEvent();
foo.AnEvent -= evt;

И да, если вы этого не сделаете, у вас будет утечка памяти, поскольку вы каждый раз будете подключать новый объект делегата. Вы также заметите это, потому что каждый раз, когда вы вызываете этот метод, он выводит на консоль все большее количество строк (не только увеличивающееся число, но и для одного вызова MethodThatFiresAnEvent, он сбрасывает любое количество элементов один раз за каждый подключил анонимный метод).

Что ж, вы можете расширить то, что здесь было сделано, чтобы сделать делегатов более безопасными в использовании (без утечек памяти)

Да, точно так же, как обычные обработчики событий могут вызывать утечки. Поскольку лямбда фактически изменена на:

someobject.SomeEvent += () => ...;
someobject.SomeEvent += delegate () {
    ...
};

// unhook
Action del = () => ...;
someobject.SomeEvent += del;
someobject.SomeEvent -= del;

Так что по сути это всего лишь сокращение от того, что мы использовали в 2.0 все эти годы.

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

Ваш пример просто компилируется в частный внутренний класс с именем компилятора (с полем firedCount и методом с именем компилятора). Каждый вызов PotentialMemoryLeaker создает новый экземпляр класса закрытия, на который foo сохраняет ссылку посредством делегата на единственный метод.

Если вы не ссылаетесь на весь объект, которому принадлежит PotentialMemoryLeaker, тогда все это будет собрано сборщиком мусора. В противном случае вы можете установить для foo значение null или пустой список обработчиков событий foo, написав следующее:

foreach (var handler in AnEvent.GetInvocationList()) AnEvent -= handler;

Конечно, вам понадобится доступ к закрытым членам класса MyObject .