Вы когда-нибудь задавались вопросом о том, могут ли события быть виртуальными? Вполне возможно вас самих эта мысль никогда не посещала; возможно, вам в этом помог какой-нибудь дотошный парень на собеседование, в общем, не очень-то важно, думали вы об этом или нет, давайте подумаем над этим вопросом совместно прямо сейчас.
Итак, события в языке C# по сути являются реализацией известного паттерна publish/subscribe и содержат всего пару методов add и remove, для подписки и отписки от события, и закрытое поле с мультикаст делегатом, который, собственно, этих самых подписчиков и содержит. А раз событие – это по сути методы, а методы могут быть виртуальными, можно сделать вывод, что события тоже могут быть виртуальными (тем более что свойства ведь могут быть виртуальными и этот факт не вредит ничьему ментальному здоровью). Итак, теоретически – с виртуальными событиями все должно быть нормально, однако это как раз тот случай, когда теория с практикой несколько расходятся.
Но обо всем по порядку; давайте рассмотрим следующий пример. Предположим у вас есть некоторый базовый класс, скажем, класс Base, который содержит событие с именемSomeEvent, и есть производный класс Derived, который по какой-то причине хочет иметь возможность переопределить событие базового класса. Вполне возможно, что причина столь необычной идеи уходит корнями к безумной преданности вашего высокодушевного сознания к замечательной книге банды четырех, которую вы перечитываете длинными зимними вечерами. Так вот, в очередной раз перечитывая паттерны поведения вы осознали, что к событиям вполне можно применить паттерн «Метод шаблона» сделав непосредственный процесс подписки/отписки виртуальным, а процесс генерации события – невиртуальным, таким образом четко разделив ответственность между базовым классом и его наследником.
В результате, это идея вылилась в следующий код:
// Базовый класс с виртуальным событием. Реализация паттерна проектирования
// "Метод шаблона", когда само событие является виртуальным и переопределяется
// наследником, а процесс вызова события реализован с помощью открытого
// невиртуального метода
class Base
{
// Само вирутуальное событие
public virtual event EventHandler SomeEvent;
// Фукнция генерации события
// (сделана открытой только в демонстрационных целях)
public void InvokeSomeEvent()
{
var handler = SomeEvent;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
// Класс-наследник, переопределяющий виртуальное событие
class Derived : Base
{
// Переопределяем виртуальное событие
public override event EventHandler SomeEvent;
}
class Program
{
static void Main(string[] args)
{
Derived derived = new Derived();
derived.SomeEvent += (s, e) => Console.WriteLine("Some event handler.");
derived.InvokeSomeEvent();
Console.ReadLine();
}
}
Запускам этот код на выполнение и … не видим в консоли ничего, поскольку наш обработчик события не вызывается.
Все дело в том, что событие (field-like event) в базовом классе Base все также разворачивается в пару открытых методов add/remove и *закрытое* (private) поле делегата с типомEventHandler, который модифицируется в обозначенных выше методах. И когда компилятор встречает имя такого события в коде, то он трактует его по разному, в зависимости от того, где находится это обращение: «снаружи» этого класса или «внутри» него. Если обращение к событию происходит внутри класса, то ты вместо события как такового мы обращаемся к нижележащему делегату, а если же обращение происходит снаружи класса – то мы «видим» наше событие через «призму» двух методов подписки/отписки. Это сделано специально для разграничения ответственности: внешний код может лишь подписываться и отписываться на событие, а вот «зажигать» событие или его модифицировать напрямую (установив его в null, например) – не может. Но проблема заключается в том, что в нашем случае то же самое происходит и в классе Derived: для него также генерируется свое собственное закрытое поле и его именно оно изменяется в классе Derived.
Таким образом, компилятор генерирует примерно следующий код:
class Base
{
// Реализуем событие "вручную"
public virtual event EventHandler SomeEvent
{
// Обращаемся к своему собственному закрытому полю
add { _baseClassBackingDelegate += value; }
remove { _baseClassBackingDelegate -= value; }
}
// Фукнция генерации события
private void InvokeSomeEvent()
{
// Обращаемся к полю делегата, а не к имени события,
// поскольку вызов делегата осуществляется именно через поле делегата
var handler = _baseClassBackingDelegate;
if (handler != null)
handler(this, EventArgs.Empty);
}
// Поле делегата является закрытым, а не защищенным,
// что делает невозможным подписку/описку именно к этому полю
// из класса наследника
private EventHandler _baseClassBackingDelegate;
}
class Derived : Base
{
// Переопределяем событие
public override event EventHandler SomeEvent
{
// Обращаемся к собственному закрытому полю
add { _derivedClassBackingDelegate += value; }
remove { _derivedClassBackingDelegate -= value; }
}
// В классе наследнике объявляется еще одно поле делегата
private EventHandler _derivedClassBackingDelegate;
}
Теперь должно быть понятно, почему наше событие не было вызвано: наш метод обратного вызова из-за переопределения события в классе Derived был добавлен в поле делегата производного класса, а метод InvokeSomeEvent вызывает все методы обратного вызова, сохраненные в классе Base.
Так уж выходит, что наша попытка сделать виртуальное field-like событие с невиртуальным методом генерации этого события приводит именно туда, куда ведут многие другие благие намерения, а именно к трудноуловимым ошибкам и головной боли наших коллег, которые будут поддерживать этот код в будущем. Основная идея наследования вообще и применение паттерна проектирования "Метод шаблона” в частности, направлены на то, чтобы сделать отношения между базовым классом и наследником простыми и понятными, такими, чтобы базовый класс было легко использовать правильно и сложно – неправильно. В случае же с виртуальными событиями сложно вообще понять, какая именно роль и обязанности возлагаются на базовый класс, а какая – на производный. И хотя использование виртуальных событий все же возможно (*) (если делать это осторожно и со знанием дела), я бы предпочел более четкий контракт между базовым классом и его наследниками в виде более конкретных и понятных виртуальных функций.
P.S. Нужно отметить, что хотя теоретически компилятор мог бы определять подобные ситуации и генерировать защищенное поле в базовом классе, но делать этого никто не будет по одной простой причине: подобные изменения будут ломающими (так называемые breaking changes). Просто представьте себе, что у кого-то в продакшне находится подобный код с виртуальным событием, но с дополнительной функцией InvokeSomeEvent в производном классе. Тогда, после перехода на новую версию языка C#, начнет вызываться дополнительный код, который до этого не вызывался и не тестировался, что может привести к непредсказуемому поведению приложения и или непонятным ошибкам во время выполнения.
АВТОР: СЕРГЕЙ ТЕПЛЯКОВ
Статья позаимствована с сайта: sergeyteplyakov.blogspot.com