Вот представьте себе, начинает кто-то работать .Net программистом. Изучает хорошо основы языка C#, разбирается с различиями значимых и ссылочных типов, даже хорошо понимает работу Garbage Collector'а и зачем нужны эфемерные сегменты. А спустя долгие годы, если с чем-то ему не приходится сталкиваться ежедневно, то оно напрочь забывается. Я легко могу представить, как веб-разработчик забудет синтаксис объявления событий – ведь они практически не используются в stateless парадигме веб-проектов. Если, по каким-то причинам, вы забыли эти особенности, то прошу под кат.
Disclaimer: это не исчерпывающее описание механизма событий в .Net, а просто удобный способ вспомнить что такое делегаты, события и некоторые практические трюки при работе с ними.
Совет: если не хочется возиться в песочнице и вспоминать элементарные основы, то рекомендую не пропустить интересный трюк, представленный в последнем разделе “Бронебойный Event”.
Делегаты
В C++ есть указатели на функции, в которые можно сохранить адрес любой функции и потом, косвенно обратившись к нему, мы вызываем саму функцию. В дот нете решили пойти дальше, и ввели понятие делегатов. Это основные строительные блоки, на основе которых строится механизм событий. Они по сути являются строго типизированными объектами, описывающие сигнатуру метода и способные содержать ссылки на совместимые по сигнатуре методы. Проверка совместимости сигнатуры метода является одним из основных преимуществ над указателями в C++.
Что бы сохранить ссылку мы должны:
- Создать тип делегата (определить сигнатуру совместимого метода)
- Объявить переменную типа делегата (в ней будет храниться экземпляр делегата)
- Добавить ссылку на метод в переменную типа делегата.
Давайте рассмотрим это на примере в листинге №1:
Листинг №1
public class MyClass
{
public void Method(string message)
{
Console.WriteLine("Method: {0}", message);
}
public static void StaticMethod(string message)
{
Console.WriteLine("StaticMethod: {0}", message);
}
}
class Program
{
// объявляем тип делегата
public delegate void MyDelegate(string message);
// создаём переменную типа делегата
private static MyDelegate delegateInstance;
static void Main(string[] args)
{
var classInstance = new MyClass();
// STEP 1: помещаем одну ссылку на StaticMethod
delegateInstance = new MyDelegate(MyClass.StaticMethod);
// STEP 2: затираем первую ссылку и заменяем её новым значением
delegateInstance = new MyDelegate(classInstance.Method);
// STEP 3: затираем первую ссылку и заменяем её новым значением
delegateInstance = message =>
{
Console.WriteLine("Anonymous: {0}", message);
};
// STEP 4: делаем тоже, но по старинке (если версия .Net ниже 3.0)
delegateInstance = delegate(string message)
{
Console.WriteLine("Anonymous: {0}", message);
};
// STEP 5: добавляем (+= вместо =) вторую ссылку
delegateInstance += new MyDelegate(classInstance.Method);
// STEP 6: добавляем третью ссылку (пользуясь сокращённым синтаксисом)
delegateInstance += MyClass.StaticMethod;
// STEP 7: убираем ссылку третью ссылку (за раз убирается только одна ссылка!)
delegateInstance -= classInstance.Method;
// STEP 8: можем ещё раз попробывать убрать ссылку, но
// ничего не произойдёт, т.к. больше нет ссылок на Method
delegateInstance -= classInstance.Method;
// вызываем делагат
delegateInstance("Hello world");
// РЕЗУЛЬТАТ:
// "Anonymous: Hello world" (STEP 4)
// "StaticMethod: Hello world" (STEP 6)
//
// ---------------------------------------------------------------------------------------
// State after STEP | Target propery | Method propery | _invocationListCount field |
// ---------------------------------------------------------------------------------------
// 1 | null | StaticMethod | 0 |
// 2 | classInstance | Method | 0 |
// 3 | null | anonymousMethod_1 | 0 |
// 4 | null | anonymousMethod_2 | 0 |
// 5 | classInstance | Method | 2 |
// 6 | null | StaticMethod | 3 |
// 7 | null | StaticMethod | 2 |
// 8 | null | StaticMethod | 2 |
}
Интересные моменты:
- Если ссылка на метод не установлена, то переменная содержащая делегат считается не инициализированной и равна null.
- Если добавить ссылку, то в переменной будет содержаться объект делегата, в свойстве Method которого будет сохранена ссылка на метод.
- Если метод экземплярный, то ссылка на экземпляр объекта, будет сохранена во свойстве Target
- Если ссылок больше одной, то они содержатся в приватном поле _invocationList, а значения свойств Target и Method соответствует последней добавленной ссылке
- Если ссылок меньше двух, то поле _invocationList равно NULL
- От ссылки можно отписывать сколько угодно раз
- Если ссылок на один и тот же метод несколько, то раз удаляется только одна ссылка
- При попытки отписки от несуществующей ссылки исключение НЕ генерируется
- CLR не гарантирует порядок вызова ссылок, поэтому нельзя на него полагаться
Event
Зачем нужны события, если есть делегаты? Для ответа на этот вопрос, давайте попробуем создать событие, на основе делегата. Результат будет похож на пример из листинга №2.
Листинг №2.
public class Employee
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
RaiseOnNameChanged();
}
}
// объявляем тип делегата
public delegate void NameChangedDelegate(string newValue);
// создаём публичное свойство
public NameChangedDelegate NameChanged { get; set; }
// вспомогательный метод для сигнализирования собитиия
private void RaiseOnNameChanged()
{
// проверяем, что есть подписчики
if (NameChanged != null)
{
// сообщаем подписчикам о произошедшем изменении
NameChanged(_name);
}
}
}
Проблема в том, что если мы откроем доступ к полю (или свойству) делегата то любой подписчик сможет нарушить работу остальных подписчиков. Это произойдёт, если вместо добавления себя в цепочку вызова (+=), какой-то подписчик присвоит новое значение (=). Кроме того, оставляя делегат публичным для возможности подписки, мы позволим любому классу сгенерировать событие – это явно противоречит принципу инкапсуляции и здравому смыслу.
Поэтому на арене появляется ключевое слово event. Перейдя по ссылке можно посмотреть полный пример использования этого ключевого слова. Я же хотел бы остановиться на наиболее интересной его части:
public event SampleEventHandler SampleEvent;
protected virtual void RaiseSampleEvent()
{
if (SampleEvent != null)
SampleEvent(this, new SampleEventArgs("Hello"));
}
Стоит обратить внимание на ряд моментов:
- Производится проверка на не равенство NULL - if (SampleEvent != null). Это необходимо для предотвращения NullReferenceException в случае, если у нас нет подписчиков.
- Если приложение многопоточное, то между моментом описанным в пункте 1 и фактическим использованием делегата может произойти переключение потоков и испортиться значение (отписка, приводящая к установке NULL в SampleEvent) и в итоге мы всё таки получим NullReferenceException, от которого пытались защититься проверкой на NULL.
- Если приложение исполняется под управлением .Net Framework версии ниже 4.0, то подписка на событие может привести к ситуации с DeadLock’ом. Это происходит из-за того, что компилятор, пытаясь делать подписку потоко-безопасной, для синхронизации доступа к полю делегата использует конструкцию lock(this), а это большой !No-no-no! в многопоточном приложении.
Что бы избавиться от проблемы, описанной в пункте №2 необходимо перед проверкой на NULL, сохранить значение делегата во временную переменную. Тогда даже если все отпишутся и поле делегата будет NULL, то приложение всё равно отработает корректно и NullReferenceException не произойдёт.
Что бы избавиться от проблемы из пункта №3 нужно писать свои подписчик и отписчик и лочить не this, а какой-то приватный член.
Вот как это выглядит вместе:
private SampleEventHandler _sampleEvent;
public event SampleEventHandler SampleEvent
{
add
{
lock (_sampleEvent)
{
_sampleEvent += value;
}
}
remove
{
lock (_sampleEvent)
{
_sampleEvent -= value;
}
}
}
protected virtual void RaiseSampleEvent()
{
var temp = _sampleEvent;
if (temp != null)
{
temp(this, new SampleEventArgs("Hello"));
}
}
Но у нас остаётся ещё одна проблема, не описанная выше. Если хоть один из подписавшихся подписчиков сгенерирует исключение, то вся цепочка оборвётся и не вызванные ещё подписчики так и не получат уведомление о событии. Для гарантированного вызова всех подписавшихся методов необходимо вручную обойти весь список подписчиков и в случае исключения, корректно его обработать. Как это сделать будет показано в следующем разделе.
Бронебойный Event
public event SampleEventHandler SampleEvent = delegate { };
protected virtual void RaiseSampleEvent()
{
SampleEvent(this, new SampleEventArgs("Hello"));
}
protected virtual void RaiseSampleEventRobust()
{
var exceptions = new List<Exception>();
foreach (var handler in SampleEvent.GetInvocationList())
{
try
{
handler.DynamicInvoke(this, new SampleEventArgs("Hello"));
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any())
{
throw new AggregateException(exceptions);
}
}
Особое внимание хочу обратить на небольшой трюк, который применён в первой строке. При объявлении еvent’а мы сразу же присваиваем пустой делегат. Это избавляет нас от необходимости производить проверки на NULL, перед обращением к делегату. Так что, если вы не хотите гарантировать 100% вызов всех подписчиков, то вполне корректной реализацией ивента и метода его генерирования будет код из всего 4-х строк, 2 из которых будут ‘{‘ и ‘}’.
Комментариев нет:
Отправить комментарий