Сборка мусора при использовании анонимных делегатов для обработки событий
ОБНОВИТЬ
Отсюда я соединил различные ответы в "окончательный" ответ на новый вопрос.
Оригинальный вопрос
В моем коде у меня есть издатель событий, который существует на протяжении всего времени жизни приложения (здесь он сокращен до базовых):
public class Publisher
{
//ValueEventArgs<T> inherits from EventArgs
public event EventHandler<ValueEventArgs<bool>> EnabledChanged;
}
Поскольку этот издатель может использоваться повсеместно, я был очень доволен собой за создание этого небольшого вспомогательного класса, чтобы избежать переписывания кода обработки для всех подписчиков:
public static class Linker
{
public static void Link(Publisher publisher, Control subscriber)
{
publisher.EnabledChanged += (s, e) => subscriber.Enabled = e.Value;
}
//(Non-lambda version, if you're not comfortable with lambdas)
public static void Link(Publisher publisher, Control subscriber)
{
publisher.EnabledChanged +=
delegate(object sender, ValueEventArgs<bool> e)
{
subscriber.Enabled = e.Value;
};
}
}
Он работал нормально, пока мы не начали использовать его на небольших машинах, когда я начал получать случайные:
System.ComponentModel.Win32Exception
Not enough storage is available to process this command
Оказывается, в коде есть одно место, где элементы управления подписчиками динамически создаются, добавляются и удаляются из формы. Учитывая мое глубокое понимание процесса сбора мусора и т. Д. (Т. Е. До вчерашнего дня), я никогда не думал о том, чтобы прояснить ситуацию позади себя, поскольку в подавляющем большинстве случаев подписчики также живут в течение всего срока службы приложения.
Некоторое время я возился с WeakEventHandler Дастина Кэмпбелла, но он не работает с анонимными делегатами (во всяком случае, не для меня).
Есть ли вообще выход из этой проблемы? Я действительно хотел бы избежать необходимости копировать и вставлять код котельной пластины по всему магазину.
(О, и не спрашивайте меня, ПОЧЕМУ мы все время создаем и уничтожаем элементы управления, это было не мое дизайнерское решение...)
(PS: Это приложение winforms, но мы обновились до VS2008 и.Net 3.5. Стоит ли использовать шаблон Weak Event?)
(PPS: хороший ответ от Рори, но если кто-нибудь может придумать эквивалент WeakEventHandler, который избавит меня от необходимости помнить о явном UnLink/Dispose, это было бы круто...)
РЕДАКТИРОВАТЬ Я должен признать, что я работал над этой проблемой, "перерабатывая" соответствующие элементы управления. Однако обходной путь вернулся, чтобы преследовать меня, поскольку "ключ", который я использовал, по-видимому, не уникален (всхлип). Я только что обнаружил другие ссылки здесь (пробовал это - кажется, слишком слабым - GC очищает делегатов, даже если цель все еще жива, та же проблема с s,oɔɯǝɹ ответ ниже), здесь (вынуждает вас изменять издателя, и не не работает с анонимными делегатами) и здесь (цитируется Дастином Кэмпбеллом как неполное).
Мне приходит в голову, что то, что я ищу, может быть семантически невозможным - затворы предназначены для того, чтобы "торчать даже после того, как я уйду".
Я нашел другой обходной путь, поэтому я буду придерживаться этого, ожидая голоса богов.
4 ответа
Я знаю, что этот вопрос древний, но, черт возьми, я его нашел, и думаю, что другие тоже могут. Я пытаюсь решить связанную проблему, и, возможно, у меня есть понимание.
Вы упомянули WeakEventHandler Дастина Кэмпбелла - он действительно не может работать с анонимными методами. Я пытался совместить что-то, что могло бы произойти, когда я понял, что а) в 99% случаев мне нужно что-то подобное, его первоначальное решение будет безопаснее, и б) в тех немногих случаях, когда я должен (примечание: есть чтобы, а не "хочу, потому что лямбды намного красивее и лаконичнее") можно заставить его работать, если вы станете немного умнее.
Ваш пример выглядит как единовременный случай, когда немного сложнее может привести к довольно краткому решению.
public static class Linker {
public static void Link(Publisher publisher, Control subscriber) {
// anonymous method references the subscriber only through weak
// references,so its existance doesn't interfere with garbage collection
var subscriber_weak_ref = new WeakReference(subscriber);
// this instance variable will stay in memory as long as the anonymous
// method holds a reference to it we declare and initialize it to
// reserve the memory (also, compiler complains about uninitialized
// variable otherwise)
EventHandler<ValueEventArgs<bool>> handler = null;
// when the handler is created it will grab references to the local
// variables used within, keeping them in memory after the function
// scope ends
handler = delegate(object sender, ValueEventArgs<bool> e) {
var subscriber_strong_ref = subscriber_weak_ref.Target as Control;
if (subscriber_strong_ref != null)
subscriber_strong_ref.Enabled = e.Value;
else {
// unsubscribing the delegate from within itself is risky, but
// because only one instance exists and nobody else has a
// reference to it we can do this
((Publisher)sender).EnabledChanged -= handler;
// by assigning the original instance variable pointer to null
// we make sure that nothing else references the anonymous
// method and it can be collected. After this, the weak
// reference and the handler pointer itselfwill be eligible for
// collection as well.
handler = null;
}
};
publisher.EnabledChanged += handler;
}
}
Ходят слухи, что шаблон слабых событий WPF сопряжен с большими накладными расходами, поэтому в этой конкретной ситуации я бы его не использовал. Более того, ссылки на основную библиотеку WPF в приложении WinForm также кажутся немного тяжелыми.
Если вы сохраняете ссылку на анонимного делегата, а затем удаляете ее, когда элементы управления удаляются из формы, это должно позволить собирать мусор как элементам управления, так и анонимным делегатам.
Так что-то вроде этого:
public static class Linker
{
//(Non-lambda version, I'm not comfortable with lambdas:)
public static EventHandler<ValueEventArgs<bool>> Link(Publisher publisher, Control subscriber)
{
EventHandler<ValueEventArgs<bool>> handler = delegate(object sender, ValueEventArgs<bool> e)
{
subscriber.Enabled = e.Value;
};
publisher.EnabledChanged += handler;
return handler;
}
public static void UnLink(Publisher publisher, EventHandler<ValueEventArgs<bool>> handler)
{
publisher.EnabledChanged -= handler;
}
}
Посмотрите анонимный метод Unsubscribe в C# для примера удаления делегатов.
Пример кода, который я недавно сделал на основе WeakReference:
// strongly typed weak reference
public class WeakReference<T> : WeakReference
where T : class
{
public WeakReference(T target)
: base(target)
{ }
public WeakReference(T target, bool trackResurrection)
: base(target, trackResurrection)
{ }
public new T Target
{
get { return base.Target as T; }
set { base.Target = value; }
}
}
// weak referenced generic event handler
public class WeakEventHandler<TEventArgs> : WeakReference<EventHandler<TEventArgs>>
where TEventArgs : EventArgs
{
public WeakEventHandler(EventHandler<TEventArgs> target)
: base(target)
{ }
protected void Invoke(object sender, TEventArgs e)
{
if (Target != null)
{
Target(sender, e);
}
}
public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> weakEventHandler)
{
if (weakEventHandler != null)
{
if (weakEventHandler.IsAlive)
{
return weakEventHandler.Invoke;
}
}
return null;
}
}
// weak reference common event handler
public class WeakEventHandler : WeakReference<EventHandler>
{
public WeakEventHandler(EventHandler target)
: base(target)
{ }
protected void Invoke(object sender, EventArgs e)
{
if (Target != null)
{
Target(sender, e);
}
}
public static implicit operator EventHandler(WeakEventHandler weakEventHandler)
{
if (weakEventHandler != null)
{
if (weakEventHandler.IsAlive)
{
return weakEventHandler.Invoke;
}
}
return null;
}
}
// observable class, fires events
public class Observable
{
public Observable() { Console.WriteLine("new Observable()"); }
~Observable() { Console.WriteLine("~Observable()"); }
public event EventHandler OnChange;
protected virtual void DoOnChange()
{
EventHandler handler = OnChange;
if (handler != null)
{
Console.WriteLine("DoOnChange()");
handler(this, EventArgs.Empty);
}
}
public void Change()
{
DoOnChange();
}
}
// observer, event listener
public class Observer
{
public Observer() { Console.WriteLine("new Observer()"); }
~Observer() { Console.WriteLine("~Observer()"); }
public void OnChange(object sender, EventArgs e)
{
Console.WriteLine("-> Observer.OnChange({0}, {1})", sender, e);
}
}
// sample usage and test code
public static class Program
{
static void Main()
{
Observable subject = new Observable();
Observer watcher = new Observer();
Console.WriteLine("subscribe new WeakEventHandler()\n");
subject.OnChange += new WeakEventHandler(watcher.OnChange);
subject.Change();
Console.WriteLine("\nObserver = null, GC");
watcher = null;
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
subject.Change();
if (Debugger.IsAttached)
{
Console.Write("Press any key to continue . . . ");
Console.ReadKey(true);
}
}
}
Создает следующий вывод:
new Observable()
new Observer()
subscribe new WeakEventHandler()
DoOnChange()
-> Observer.OnChange(ConsoleApplication4.Observable, System.EventArgs)
Observer = null, GC
~Observer()
DoOnChange()
~Observable()
Press any key to continue . . .
(Обратите внимание, что отписаться (-=) не работает)
Опираясь на ответ Егора, я хотел попробовать создать версию, в которой мне не нужно было заранее определять, к какому событию я хочу присоединиться.
Мне удалось заставить его работать только с общими обработчиками событий: для "стандартных" обработчиков событий (например, FormClosingEventHandler) это немного сложно, потому что вы не можете иметь ограничение типа where T : delegate
(если ваше имя не заканчивается на Пони).
private static void SetAnyGenericHandler<S, T>(
Action<EventHandler<T>> add, //to add event listener to publisher
Action<EventHandler<T>> remove, //to remove event listener from publisher
S subscriber, //ref to subscriber (to pass to consume)
Action<S, T> consume) //called when event is raised*
where T : EventArgs
where S : class
{
var subscriber_weak_ref = new WeakReference(subscriber);
EventHandler<T> handler = null;
handler = delegate(object sender, T e)
{
var subscriber_strong_ref = subscriber_weak_ref.Target as S;
if(subscriber_strong_ref != null)
{
Console.WriteLine("New event received by subscriber");
consume(subscriber_strong_ref, e);
}
else
{
remove(handler);
handler = null;
}
};
add(handler);
}
(* Я пытался EventHandler<T> consume
здесь, но вызывающий код становится уродливым, потому что вы должны привести s к Subscriber в лямбда-потреблении.)
Пример кода вызова, взятый из примера выше:
SetAnyGenericHandler(
h => publisher.EnabledChanged += h,
h => publisher.EnabledChanged -= h,
subscriber,
(Subscriber s, ValueEventArgs<bool> e) => s.Enabled = e.Value);
Или, если вы предпочитаете
SetAnyGenericHandler<Subscriber, ValueEventArgs<bool>>(
h => publisher.EnabledChanged += h,
h => publisher.EnabledChanged -= h,
subscriber,
(s, e) => s.Enabled = e.Value);
Было бы неплохо иметь возможность передавать Событие как один параметр, но вы не можете получить доступ к добавлению / удалению из события так же, как вы не можете получить доступ к извлечению / установке из свойства (я думаю, что я не делаю штуковины с отвратительной рефлексией)).