Создание событий в C# с использованием обобщений (generics)

События в 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-го издания.

Полезная статья? Их будет больше, если вы поддержите меня!

https://money.yandex.ru/embed/small.xml?account=410011399843532&quickpay=small&yamoney-payment-type=on&button-text=06&button-size=l&button-color=orange&targets=%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D1%8E+%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82+lexpenz.com&default-sum=50&successURL=