Слабые события в C# - Часть 2: Слабые события стороны источника
ОГЛАВЛЕНИЕ
Часть 2: Слабые события стороны источника
Рассмотрим способы реализации слабых событий путем изменения источника событий.Все они имеют общее преимущество над слабыми событиями стороны слушателя: регистрацию/снятие с регистрации обработчиков легко сделать поточно-ориентированными.
Решение 0: Интерфейс
WeakEventManager также стоит упомянуть в данном разделе: как обертка, он прикрепляется ("сторона слушателя") к нормальным событиям C#, но также предоставляет ("сторона источника") слабое событие клиентам.В WeakEventManager это интерфейс IWeakEventListener. Слушающий объект реализует интерфейс, а источник имеет слабую ссылку на слушателя и вызывает метод интерфейса.
Плюсы
Простой и эффективный.
Минусы
Когда слушатель обрабатывает несколько событий, вы в итоге получаете множество условий в методе HandleWeakEvent, из которых нужно отбирать тип события и источник события.
Решение 1: Слабая ссылка на делегат
Это другой подход к слабым событиям, используемый в WPF: CommandManager.InvalidateRequery выглядит как нормальное событие .NET, но не является таковым. Он хранит только слабую ссылку на делегат, поэтому регистрация в то статическое событие не вызывает утечек памяти.
Это простое решение, но потребитель события с легкостью может забыть о нем и ошибиться:
CommandManager.InvalidateRequery += OnInvalidateRequery;
//или
CommandManager.InvalidateRequery += new EventHandler(OnInvalidateRequery);
Проблема в том, что CommandManager хранит только слабую ссылку на делегат, а слушатель вообще не хранит ссылки на него. Во время следующего запуска GC делегат будет удален, и OnInvalidateRequery больше не будет вызываться, даже если объект слушателя все еще используется. Чтобы гарантировать достаточно долгое существование делегата, слушатель обязан хранить ссылку на него.
class Listener {WeakReferenceToDelegate в загруженном исходном коде показывает пример реализации события, являющегося поточно-ориентированным, и очищает список обработчиков, когда добавляется другой обработчик.
EventHandler strongReferenceToDelegate;
public void RegisterForEvent()
{
strongReferenceToDelegate = new EventHandler(OnInvalidateRequery);
CommandManager.InvalidateRequery += strongReferenceToDelegate;
}
void OnInvalidateRequery(...) {...}
}
Плюсы
Отсутствует утечка экземпляров делегатов.
Минусы
Легко ошибиться: забывание жесткой ссылки на делегат приводит к тому, что события возбуждаются только до следующей сборки мусора. Это может вызывать трудно обнаруживаемые ошибки.
Решение 2: объект + механизм переадресации данных
В то время как решение 0 было основано на WeakEventManager, это решение основано на обертке WeakEventHandler: регистрируется пара object (объект),ForwarderDelegate.
eventSource.AddHandler(this,
(me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));
Плюсы
Простое и эффективное.
Минусы
Необычная сигнатура для регистрации событий; переадресация лямбда-выражений требует преобразования типов.
Решение 3: Разумное слабое событие
SmartWeakEvent в загруженном исходном коде предоставляет событие, выглядящее как нормальное событие .NET, но хранящее слабые ссылки на слушатель события. Он не страдает от проблемы "должен хранить ссылку на делегат".
void RegisterEvent()
{
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
...
}
Определение события:
SmartWeakEvent<EventHandler> _event
= new SmartWeakEvent<EventHandler>();
public event EventHandler Event {
add { _event.Add(value); }
remove { _event.Remove(value); }
}
public void RaiseEvent()
{
_event.Raise(this, EventArgs.Empty);
}
Как это работает? Путем использования свойств Delegate.Target и Delegate.Method каждый делегат разделяется на цели (хранится как слабая ссылка) и MethodInfo. Когда возбуждается событие, метод вызывается при помощи Отражения.
Возможная проблема в том, что кто-то может попытаться прикрепить безымянный метод как обработчик события, захватывающий переменную.
int localVariable = 42;
eventSource.Event += delegate { Console.WriteLine(localVariable); };
При этом целевой объект делегата - замкнутое выражение, которое может быть сразу же удалено, так как на него нет других ссылок. Однако SmartWeakEvent может обнаружить эту ситуацию и сгенерирует исключение, поэтому устранять проблемы будет нетрудно, так как обработчик события снимается с регистрации раньше, чем это должно было произойти.
if (d.Method.DeclaringType.GetCustomAttributes(
typeof(CompilerGeneratedAttribute), false).Length != 0)
throw new ArgumentException(...);
Плюсы
Выглядит как настоящее слабое событие; почти нет лишнего кода.
Минусы
Вызов с использованием отражения медленный; не работает в неполном доверии, так как использует отражение на закрытых методах.
Решение 4: Быстрое разумное слабое событие
Функциональность и использование тождественны SmartWeakEvent, но производительность намного выше.
Здесь показаны результаты программы оценки производительности события с двумя зарегистрированными делегатами (один метод экземпляра и один статический метод):
нормальное (сильное) событие... 16948785 вызовов в секунду
разумное слабое событие... 91960 вызовов в секунду
быстрое разумное слабое событие... 4901840 вызовов в секунду
Как это работает? Отражение больше не используется для вызова метода. Вместо этого компилируется переадресующий метод (подобно "переадресующему коду" в предыдущих решениях) во время выполнения при помощи System.Reflection.Emit.DynamicMethod.
Плюсы
Выглядит как настоящее слабое событие; почти нет лишнего кода.
Минусы
Не работает в неполном доверии, так как использует отражение на закрытых методах.
Советы
• Для всего, что выполняется в потоке пользовательского интерфейса в приложениях WPF (например, специальные элементы управления, прикрепляющие события к объектам-моделям) – используйте WeakEventManager.
• Если нужно предоставить слабое событие – используйте FastSmartWeakEvent.
• Если нужно потреблять событие – используйте WeakEventHandler.