Слабые события в C#
ОГЛАВЛЕНИЕ
Страница 1 из 3
В данной статье мы обсудим разные подходы к слабым событиям. Введение
При использовании нормальных событий C# регистрация обработчика события создает жесткую ссылку от источника события на слушающий объект.Если объект-источник имеет более долгое время жизни, чем слушатель, и слушателю больше не нужны события, когда нет других ссылок на него, использование нормальных событий .NET вызывает утечку памяти: объект-источник удерживает в памяти объекты-слушатели, которые должны быть удалены сборщиком мусора.
Есть много разных подходов к этой проблеме. Данная статья объясняет некоторые из них и рассматривает их плюсы и минусы. Подходы были разделены на две категории: сначала предполагается, что источник события – существующий класс с нормальным событием C#; после этого разрешается изменение источника события, чтобы позволить разные подходы.
Чем именно являются события?
Многие программисты считают события списком делегатов – это неверно. Делегаты сами могут быть групповыми (многоадресными):EventHandler eh = Method1;Так что же такое события? По сути, они похожи на свойства: они инкапсулируют поле делегата и ограничивают доступ к нему. Открытое поле делегата (или открытое свойство делегата) может означать, что другие объекты могут очистить список обработчиков событий, или возбуждать событие – но нужно, чтобы только создавший событие объект мог делать это.
eh += Method2;
В сущности, свойства – пара методов get (прочитать)/set (установить). События – пара методов add (добавить)/remove (удалить).
public event EventHandler MyEvent {Только добавление и удаление обработчиков public (открытое). Другие классы не могут запрашивать список обработчиков, не могут очистить список, или не могут вызвать событие.
add { ... }
remove { ... }
}
К путанице иногда приводит то, что C# имеет сокращенный синтаксис:
public event EventHandler MyEvent;Это разворачивается в:
private EventHandler _MyEvent; // подразумеваемое полеСтандартные события C# зафиксированы на this (этот)! Это можно проверить с помощью дизассемблера - методы add и remove декорированы [MethodImpl(MethodImplOptions.Synchronized)], что эквивалентно фиксации на this.
// оно на самом деле называется не "_MyEvent", а "MyEvent",
// но вы не можете увидеть разницу между полем
// и событием.
public event EventHandler MyEvent {
add { lock(this) { _MyEvent += value; } }
remove { lock(this) { _MyEvent -= value; } }
}
Регистрация и снятие с регистрации событий поточно-ориентированы. Но возбуждение события с ориентацией на многопоточное исполнение оставлено на усмотрение программиста, пишущего код, который возбуждает событие, и часто делает это неверно: код возбуждения, вероятно, используемый большинством, не ориентирован на многопоточное исполнение:
if (MyEvent != null)Вторая наиболее часто встречающаяся стратегия – первое считывание делегата события в локальную переменную.
MyEvent(this, EventArgs.Empty);
// может прерваться выполнение с исключением NullReferenceException
// когда одновременно удаляется обработчик последнего события.
EventHandler eh = MyEvent;Является ли это поточно-ориентированным? Ответ: когда как. Согласно модели памяти в спецификации C#, это не поточно-ориентировано. Компилятору JIT разрешается убирать локальную переменную, смотрите Понимание воздействия методов низкой блокировки в многопоточных приложениях. Однако среда выполнения Microsoft .NET имеет более устойчивую модель памяти (начиная с версии 2.0), и в ней этот код поточно-ориентированный. Он также является поточно-ориентированным в Microsoft .NET 1.0 и 1.1, но это не описанная в документации деталь реализации.
if (eh != null) eh(this, EventArgs.Empty);
Корректное решение, согласно спецификации ECMA, должно бы перемещать присваивание локальной переменной в блок lock(this) или использовать поле volatile (временный) для хранения делегата.
EventHandler eh;Это означает, что нужно различать события, являющиеся поточно-ориентированными, и события, не являющиеся таковыми.
lock (this) { eh = MyEvent; }
if (eh != null) eh(this, EventArgs.Empty);