Слабые события в 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 {
    EventHandler strongReferenceToDelegate;
    public void RegisterForEvent()
    {
        strongReferenceToDelegate = new EventHandler(OnInvalidateRequery);
        CommandManager.InvalidateRequery += strongReferenceToDelegate;
    }
    void OnInvalidateRequery(...) {...}
}
WeakReferenceToDelegate в загруженном исходном коде показывает пример реализации события, являющегося поточно-ориентированным, и очищает список обработчиков, когда добавляется другой обработчик.

Плюсы
Отсутствует утечка экземпляров делегатов.

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

Решение 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.