События в C# используются повсеместно и многие знакомятся с ними в самом начале изучения этого языка. В этом посте я не буду рассказывать основы и базовые принципы создания событий в C# и в .NET. Вместо этого, я расскажу, как создать обобщения (generic) для событий.
К вопросу создания дженерика для событий я пришёл из-за работы над дипломом, где мне нужно создавать много событий, отличающихся только передаваемыми аргументами. При этом, логика их создания и вызова была идентична. Но аргументы могли быть различными, причём в будущем их разнообразие должно только увеличиваться. На помощь пришёл всемогущий Google и всезнающий Рихтер.
Как известно, .NET уже содержит обобщение (generic) для делегата EventHandler
1 |
public delegate void EventHandler(object sender, TEventArgs e); |
Но TEventArgs должен получаться из EventArgs. Т.е., если вы хотите, чтобы событие содержало int, код должен быть вроде такого:
1 2 3 4 5 6 7 8 |
public class EventArgs_int : EventArgs { public int Target; public EventArgs_int(int i) { Target = i; } } |
Далее, если станет нужна строка, то понадобится писать так:
1 2 3 4 5 6 7 8 |
public class EventArgs_string : EventArgs { public string Target; public EventArgs_string(string s) { Target = s; } } |
Вам нужен отдельный класс, наследуемый от EventArgs для каждого типа объекта, который вы хотите передать с событием. Либо, придётся передавать данные с типом object:
1 2 3 4 5 6 7 8 |
public class EventArgs_object : EventArgs { public object Target; public EventArgs_object(object o) { Target = o; } } |
Этот класс работает, но подобный подход приведёт к потерям производительности на постоянном преобразовании типов и, к тому же, затруднит рефакторинг. Но выход есть.
Создадим класс, наследуемый от EventArgs с обобщённым типом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class EventArgs_Generic : EventArgs { public EventArgs_Generic(T Target) { _TargetObject = Target; } private T _TargetObject; public T TargetObject { get { return _TargetObject; } set { _TargetObject = value; } } } |
Теперь можно создать класс, который будем передавать с событием:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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) или любой другой класс:
1 |
public event EventHandler<EventArgs_Generic> SimpleObjectAdded; |
Подписка на событие и его обработка стандартны:
1 2 3 4 |
void Processor_SimpleObjectAdded(object sender, EventArgs_Generic e) { MessageBox.Show(e.TargetObject.ToString()); } |
Теперь ни что не мешает, не создавая новых классов для аргументов события, передавать в них аргументы с нужными типами. Удобно.
Чтобы сделать передачу параметров потокобезопасной, нужно предварительно добавить ссылку на делегат во временную переменную. Для удобства использования, эту логику можно инкапсулировать в методе-расширении:
1 2 3 4 5 6 7 8 9 10 11 |
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); } } |
В этом случае публикация события будет выглядеть так:
1 |
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-го издания.
Полезная статья? Их будет больше, если вы поддержите меня!