События в C# используются повсеместно и многие знакомятся с ними в самом начале изучения этого языка. В этом посте я не буду рассказывать основы и базовые принципы создания событий в C# и в .NET. Вместо этого, я расскажу, как создать обобщения (generic) для событий.
К вопросу создания дженерика для событий я пришёл из-за работы над дипломом, где мне нужно создавать много событий, отличающихся только передаваемыми аргументами. При этом, логика их создания и вызова была идентична. Но аргументы могли быть различными, причём в будущем их разнообразие должно только увеличиваться. На помощь пришёл всемогущий Google и всезнающий Рихтер.
Как известно, .NET уже содержит обобщение (generic) для делегата EventHandler
public delegate void EventHandler(object sender, TEventArgs e);
Но TEventArgs должен получаться из EventArgs. Т.е., если вы хотите, чтобы событие содержало int, код должен быть вроде такого:
public class EventArgs_int : EventArgs
{
public int Target;
public EventArgs_int(int i)
{
Target = i;
}
}
Далее, если станет нужна строка, то понадобится писать так:
public class EventArgs_string : EventArgs
{
public string Target;
public EventArgs_string(string s)
{
Target = s;
}
}
Вам нужен отдельный класс, наследуемый от EventArgs для каждого типа объекта, который вы хотите передать с событием. Либо, придётся передавать данные с типом object:
public class EventArgs_object : EventArgs
{
public object Target;
public EventArgs_object(object o)
{
Target = o;
}
}
Этот класс работает, но подобный подход приведёт к потерям производительности на постоянном преобразовании типов и, к тому же, затруднит рефакторинг. Но выход есть.
Создадим класс, наследуемый от EventArgs с обобщённым типом:
public class EventArgs_Generic : EventArgs
{
public EventArgs_Generic(T Target)
{
_TargetObject = Target;
}
private T _TargetObject;
public T TargetObject
{
get
{
return _TargetObject;
}
set
{
_TargetObject = value;
}
}
}
Теперь можно создать класс, который будем передавать с событием:
class SimpleObject
{
public SimpleObject()
{
}
private int _Value;
public int Value
{
get { return _Value; }
set { _Value = value; }
}
private string _Name;
public string Name
{
get { return _Name; }
set { _Name = value; }
}
}
Теперь создадим обработчик события используя только что определённый класс (SimpleObject) или любой другой класс:
public event EventHandler<EventArgs_Generic> SimpleObjectAdded;
Подписка на событие и его обработка стандартны:
void Processor_SimpleObjectAdded(object sender, EventArgs_Generic e)
{
MessageBox.Show(e.TargetObject.ToString());
}
Теперь ни что не мешает, не создавая новых классов для аргументов события, передавать в них аргументы с нужными типами. Удобно.
Чтобы сделать передачу параметров потокобезопасной, нужно предварительно добавить ссылку на делегат во временную переменную. Для удобства использования, эту логику можно инкапсулировать в методе-расширении:
public static class EventArgExtensions
{
public static void Raise(this TEventArgs e, Object sender, ref EventHandler eventDelegate)
{
// Копируем ссылку на делегат во временную переменную, для потоковой безопасности
EventHandler temp = Volatile.Read(ref eventDelegate);
// Если есть подписчики на это событие, уведомляем их
if (temp != null) temp(sender, e);
}
}
В этом случае публикация события будет выглядеть так:
EventArgExtensions.Raise(new EventArgs_Generic(1), sender, ref SimpleObjectAdded);
К сожалению, метод Volatile.Read доступен только начиная с .NET Framework 4.5. Из-за этого его не получится использовать при разработке на Unity3d, т.к. он до сих пор использует старую версию Mono, совместимую только с .NET Framework 3.5
По старой доброй традиции, публикую архив с работающим примером кода.
При подготовке поста использовались материалы codeproject и книги Джефри Рихтера «CLR via C#» 4-го издания.
Полезная статья? Их будет больше, если вы поддержите меня!