
Что такое событие?
Событием в языке C# называется сущность, предоставляющая две возможности: для класса — сообщать об изменениях, а для его пользователей — реагировать на них. Пример объявления события:
public event EventHandler Changed;
Рассмотрим, из чего состоит объявление. Сначала идут модификаторы события, затем ключевое слово event, после него — тип события, который обязательно должен быть типом-делегатом, и идентификатор события, то есть его имя. Ключевое слово event сообщает компилятору о том, что это не публичное поле, а специальным образом раскрывающаяся конструкция, скрывающая от программиста детали реализации механизма событий. Для того, чтобы понять, как работает этот механизм, необходимо изучить принципы работы делегатов.
Основа работы событий — делегаты
Можно сказать, что делегат в .NET — некий аналог ссылки на функцию в C++. Вместе с тем, такое определение неточно, т.к. каждый делегат может ссылаться не на один, а на произвольное количество методов, которые хранятся в списке вызовов делегата (invocation list). Тип делегата описывает сигнатуру метода, на который он может ссылаться, экземпляры этого типа имеют свои методы, свойства и операторы. При вызове метода Invoke() выполняется последовательный вызов каждого из методов списка. Делегат можно вызывать как функцию, компилятор транслирует такой вызов в вызов Invoke(). В C# для делегатов имеются операторы + и -, которые не существуют в среде .NET и являются синтаксическим сахаром языка, раскрываясь в вызов методов Delegate.Combine и Delegate.Remove соответственно. Эти методы позволяют добавлять и удалять методы в списке вызовов. Разумеется, форма операторов с присваиванием (+= и -=) также применима к операторам делегата, как и к определенным в среде .NET операторам + и — для других типов. Если при вычитании из делегата его список вызовов оказывается пуст, то ему присваивается null. Рассмотрим простой пример:
Action a = () => Console.Write("A"); //Action объявлен как public delegate void Action();
Action b = a;
Action c = a + b;
Action d = a - b;
a(); //выведет A
b(); //выведет A
c(); //выведет AA
d(); //произойдет исключение NullReferenceException, т.к. d == null
События — реализация по умолчанию
События в языке C# могут быть определены двумя способами:
Неявная реализация события (field-like event).
Явная реализация события.
Уточню, что слова “явная” и “неявная” в данном случае не являются терминами, определенными в спецификации, а просто описывают способ реализации по смыслу. Рассмотрим наиболее часто используемую реализацию событий — неявную. Пусть имеется следующий исходный код на языке C# 4 (это важно, для более ранних версий генерируется несколько иной код, о чем будет рассказано далее):
class Class {
public event EventHandler Changed;
}
Эти строчки будут транслированы компилятором в код, аналогичный следующему:
class Class {
EventHandler сhanged;
public event EventHandler Changed {
add {
EventHandler eventHandler = this.changed;
EventHandler comparand;
do {
comparand = eventHandler;
eventHandler = Interlocked.CompareExchange<EventHandler>(ref this.changed,
comparand + value, comparand);
} while(eventHandler != comparand);
}
remove {
EventHandler eventHandler = this.changed;
EventHandler comparand;
do {
comparand = eventHandler;
eventHandler = Interlocked.CompareExchange<EventHandler>(ref this.changed,
comparand - value, comparand);
} while (eventHandler != comparand);
}
}
}
Блок add вызывается при подписке на событие, блок remove — при отписке. Эти блоки компилируются в отдельные методы с уникальными именами. Оба этих метода принимают один параметр — делегат типа, соответствующего типу события и не имеют возвращаемого значения. Имя параметра всегда ”value”, попытка объявить локальную переменную с таким именем приведет к ошибке компиляции. Область видимости, указанная слева от ключевого слова event определяет область видимости этих методов. Также создается делегат с именем события, который всегда приватный. Именно поэтому мы не можем вызвать событие, реализованное неявным способом, из наследника класса. Interlocked.CompareExchange выполняет сравнение первого аргумента с третьим и если они равны, заменяет первый аргумент на второй. Это действие потокобезопасно. Цикл используется для случая, когда после присвоения переменной comparand делегата события и до выполнения сравнения другой поток изменяет этот делегат. В таком случае Interlocked.CompareExchange не производит замены, граничное условие цикла не выполняется и происходит следующая попытка.