Эти элементы используются в среде net framework для построения механизмов обработки событий
Представленный здесь перевод является вольным. Однако если под «вольным», как правило, понимают сокращённый перевод, с упущениями, упрощениями и пересказами, то здесь всё наоборот. Данный перевод является немного расширенной, уточнённой и обновлённой версией оригинала. Я выражаю огромную благодарность Сергею Теплякову aka SergeyT, который внёс неоценимый вклад в перевод и оформление данной статьи.
Типы делегатов
В каком-то смысле вы можете думать о типе делегата как о некоем интерфейсе, в котором определён лишь один метод с чётко заданной сигнатурой (в этой статье под сигнатурой метода я буду понимать все его входные и выходные (ref и out) параметры, а также возвращаемое значение). Тогда экземпляр делегата — это объект, реализующий этот интерфейс. В этом понимании, имея экземпляр делегата, вы можете вызвать любой существующий метод, сигнатура которого будет совпадать с сигнатурой метода, определённого в «интерфейсе». Делегаты обладают и другой функциональностью, но возможность делать вызовы методов с заранее определёнными сигнатурами — это и есть самая суть делегатов. Экземпляр делегата хранит ссылку (указатель, метку) на целевой метод и, если этот метод является экземплярным, то и ссылку на экземпляр объекта (класса или структуры), в котором «находится» целевой метод.
Тип делегата объявляется при помощи ключевого слова delegate . Типы делегатов могут существовать как самостоятельные сущности, так и быть объявленными внутри классов или структур. Например:
В этом примере объявлены два типа делегата. Первый — DelegateArticle.FirstDelegate , который объявлен на уровне пространства имён. Он «совместим» с любым методом, который имеет один параметр типа int и возвращает значение типа string . Второй — DelegateArticle.Sample.SecondDelegate , который объявлен уже внутри класса и является его членом. Он «совместим» с любым методом, который имеет два параметра типа char и не возвращает ничего, так как возвращаемый тип помечен как void .
Обратите внимание, что оба типа делегата имеют модификатор доступа public . Вообще, по отношению модификаторов доступа типы делегатов ведут себя так же, как классы и структуры. Если для типа делегата явно не указан модификатор доступа и этот тип объявлен внутри пространства имён, то он будет доступен для всех объектов, также находящихся внутри этого пространства имён. Если же тип делегата без модификатора объявлен внутри класса или структуры, то он будет закрытым, аналогично действию модификатора private .
При объявлении типа делегата нельзя использовать модификатор static .
Но помните, что ключевое слово delegate не всегда означает объявление типа делегата. Это же ключевое слово используется при создании экземпляров делегатов при использовании анонимных методов.
Каждый тип делегата, созданный вами, наследует члены от MulticastDelegate , а именно: один конструктор с параметрами Object и IntPtr , а также три метода: Invoke , BeginInvoke и EndInvoke . К конструктору мы вернёмся чуточку позже. Вообще-то эти три метода не наследуются в прямом смысле, так как их сигнатура для каждого типа делегата своя — она «подстраивается» под сигнатуру метода в объявленном типе делегата. Глядя на пример кода выше, выведем «наследуемые» методы для первого типа делегата FirstDelegate :
Как вы видите, возвращаемый тип методов Invoke и EndInvoke совпадает с таковым, указанным в сигнатуре делегата, так же, как и параметр метода Invoke и первый параметр BeginInvoke . Мы рассмотрим цель метода Invoke далее в статье, а BeginInvoke и EndInvoke рассмотрим в разделе, описывающем продвинутое использование делегатов. Сейчас ещё рано об этом говорить, так как мы ещё даже не знаем, как создавать экземпляры делегатов. Вот об этом и поговорим в следующем разделе.
Типы делегатов
В каком-то смысле вы можете думать о типе делегата как о некоем интерфейсе, в котором определён лишь один метод с чётко заданной сигнатурой (в этой статье под сигнатурой метода я буду понимать все его входные и выходные (ref и out) параметры, а также возвращаемое значение). Тогда экземпляр делегата — это объект, реализующий этот интерфейс. В этом понимании, имея экземпляр делегата, вы можете вызвать любой существующий метод, сигнатура которого будет совпадать с сигнатурой метода, определённого в «интерфейсе». Делегаты обладают и другой функциональностью, но возможность делать вызовы методов с заранее определёнными сигнатурами — это и есть самая суть делегатов. Экземпляр делегата хранит ссылку (указатель, метку) на целевой метод и, если этот метод является экземплярным, то и ссылку на экземпляр объекта (класса или структуры), в котором «находится» целевой метод.
Тип делегата объявляется при помощи ключевого слова delegate . Типы делегатов могут существовать как самостоятельные сущности, так и быть объявленными внутри классов или структур. Например:
В этом примере объявлены два типа делегата. Первый — DelegateArticle.FirstDelegate , который объявлен на уровне пространства имён. Он «совместим» с любым методом, который имеет один параметр типа int и возвращает значение типа string . Второй — DelegateArticle.Sample.SecondDelegate , который объявлен уже внутри класса и является его членом. Он «совместим» с любым методом, который имеет два параметра типа char и не возвращает ничего, так как возвращаемый тип помечен как void .
Обратите внимание, что оба типа делегата имеют модификатор доступа public . Вообще, по отношению модификаторов доступа типы делегатов ведут себя так же, как классы и структуры. Если для типа делегата явно не указан модификатор доступа и этот тип объявлен внутри пространства имён, то он будет доступен для всех объектов, также находящихся внутри этого пространства имён. Если же тип делегата без модификатора объявлен внутри класса или структуры, то он будет закрытым, аналогично действию модификатора private .
При объявлении типа делегата нельзя использовать модификатор static .
Но помните, что ключевое слово delegate не всегда означает объявление типа делегата. Это же ключевое слово используется при создании экземпляров делегатов при использовании анонимных методов.
Каждый тип делегата, созданный вами, наследует члены от MulticastDelegate , а именно: один конструктор с параметрами Object и IntPtr , а также три метода: Invoke , BeginInvoke и EndInvoke . К конструктору мы вернёмся чуточку позже. Вообще-то эти три метода не наследуются в прямом смысле, так как их сигнатура для каждого типа делегата своя — она «подстраивается» под сигнатуру метода в объявленном типе делегата. Глядя на пример кода выше, выведем «наследуемые» методы для первого типа делегата FirstDelegate :
Как вы видите, возвращаемый тип методов Invoke и EndInvoke совпадает с таковым, указанным в сигнатуре делегата, так же, как и параметр метода Invoke и первый параметр BeginInvoke . Мы рассмотрим цель метода Invoke далее в статье, а BeginInvoke и EndInvoke рассмотрим в разделе, описывающем продвинутое использование делегатов. Сейчас ещё рано об этом говорить, так как мы ещё даже не знаем, как создавать экземпляры делегатов. Вот об этом и поговорим в следующем разделе.
Делегаты и их роли
В данной модели событий делегаты служат для связки событий с методами, которые используются для их обработки. Делегаты позволяют другим классам записывать уведомление о событии, определяя метод обработки. При возникновении события делегат вызывает соответствующий метод. Дополнительные сведения об определении делегатов см. в разделе События.
Делегаты можно связать с одним или несколькими методами, создав так называемую многоадресную рассылку. При создании делегата для события вы (или Windows) обычно создаете событие многоадресной рассылки. Редким исключением является событие, вызывающее выполнение определенной процедуры (например, отображение диалогового окна), которая не будет логически повторяться несколько раз за событие. Сведения о том, как создать многоадресный делегат, см. в практическом руководстве Объединение делегатов (многоадресные делегаты).
Делегат многоадресной рассылки поддерживает список вызова методов, к которым он привязан. Делегат многоадресной рассылки поддерживает метод Combine, позволяющий добавить метод в список вызова, и метод Remove, позволяющий его удалить.
Когда приложение регистрирует событие, элемент управления порождает это событие, вызывая для него делегат. Делегат, в свою очередь, вызывает соответствующий метод. В самом распространенном случае (делегат многоадресной рассылки) делегат вызывает каждый метод связки из списка вызова по очереди, что обеспечивает уведомление один-ко-многим. Данная стратегия означает, что элементу управления не нужно вести список целевых объектов для уведомления о событии, поскольку записью и уведомлением занимается делегат.
Делегаты также позволяют связать с одним методом несколько событий, чтобы использовать уведомление по типу многие-к-одному. Например, событие нажатия на кнопку и событие выбора команды в меню вызывают один и тот же делегат, который вызывает один и тот же метод, обрабатывающий эти события одинаковым образом.
В делегатах используется динамический механизм связки: во время выполнения делегат может быть связан с любым методом, подпись которого совпадает с подписью обработчика событий. С помощью этой функции можно устанавливать или изменять метод связки в зависимости от условий и динамически привязывать обработчик событий к элементу управления.
Что такое событие?
Рассмотрим, из чего состоит объявление. Сначала идут модификаторы события, затем ключевое слово event, после него — тип события, который обязательно должен быть типом-делегатом, и идентификатор события, то есть его имя. Ключевое слово event сообщает компилятору о том, что это не публичное поле, а специальным образом раскрывающаяся конструкция, скрывающая от программиста детали реализации механизма событий. Для того, чтобы понять, как работает этот механизм, необходимо изучить принципы работы делегатов.
Основа работы событий — делегаты
События — реализация по умолчанию
- Неявная реализация события (field-like event).
- Явная реализация события.
Эти строчки будут транслированы компилятором в код, аналогичный следующему:
Блок add вызывается при подписке на событие, блок remove — при отписке. Эти блоки компилируются в отдельные методы с уникальными именами. Оба этих метода принимают один параметр — делегат типа, соответствующего типу события и не имеют возвращаемого значения. Имя параметра всегда ”value”, попытка объявить локальную переменную с таким именем приведет к ошибке компиляции. Область видимости, указанная слева от ключевого слова event определяет область видимости этих методов. Также создается делегат с именем события, который всегда приватный. Именно поэтому мы не можем вызвать событие, реализованное неявным способом, из наследника класса.
Объявление с указанием add и remove
Все действия по подписке и отписке (обозначаются как += и -=, можно легко спутать с операторами делегатов) компилируются в вызовы методов add и remove. Вызовы внутри класса, отличные от вышеуказанных, компилируются в простую работу с делегатом. Следует заметить, что при неявной (и при правильной явной) реализации события невозможно получить доступ к делегату извне класса, работать можно лишь с событием как с абстракцией — подписываясь и отписываясь. Так как нет способа определить, подписались ли мы на какое-либо событие (если не использовать рефлексию), то кажется логичным, что отписка от него никогда не вызовет ошибок — можно смело отписываться, даже если делегат события пуст.
Модификаторы событий
Для событий могут использоваться модификаторы области видимости (public, protected, private, internal), они могут быть перекрыты (virtual, override, sealed) или не реализованы (abstract, extern). Событие может перекрывать событие с таким же именем из базового класса (new) или быть членом класса (static). Если событие объявлено и с модификатором override и с модификатором abstract одновременно, то наследники класса должны будут переопределить его (равно как и методы или свойства с этими двумя модификаторами).
Какие типы событий бывают?
Как уже было отмечено, тип события всегда должен быть типом делегата. Стандартными типами для событий являются типы EventHandler и EventHandler где TEventArgs — наследник EventArgs. Тип EventHandler используется когда аргументов события не предусмотрено, а тип EventHandler — когда аргументы события есть, тогда для них создается отдельный класс — наследник от EventArgs. Также можно использовать любые другие типы делегатов, но применение типизированного EventHandler выглядит более логичным и красивым.
Помимо изменений в работе на разных версиях языка есть еще несколько особенностей.
Особенность №1 — продление времени жизни подписчика
При подписке на событие мы добавляем в список вызовов делегата события ссылку на метод, который будет вызван при вызове события. Таким образом, память, занимаемая объектом, подписавшимся на событие, не будет освобождена до его отписки от события или до уничтожения объекта, заключающего в себе событие. Эта особенность является одной из часто встречаемых причин утечек памяти в приложениях.
Для исправления этого недостатка часто используются weak events, слабые события. Эта тема уже была освещена на Хабре.
Особенность №2 — явная реализация интерфейса
Событие, являющееся частью интерфейса, не может быть реализовано как поле при явной реализации этого интерфейса. В таких случаях следует либо скопировать стандартную реализацию события для реализации как свойство, либо реализовывать эту часть интерфейса неявно. Также, если вам не нужна потокобезопасность этого события, можно использовать самое простое и эффективное определение:
Особенность №3 — безопасный вызов
Коротко и лаконично. Мы инициализируем делегат события пустым методом, поэтому он никогда не будет null. Вычесть из делегата этот метод невозможно, т.к. он определен при инициализации делегата и у него нет ни имени, ни ссылки на него из любого места программы.
Таким образом, мы можем вызывать события как Changed.SafeRaise(this, EventArgs.Empty), что экономит нам строчки кода. Также можно определить третий вариант метода расширений для случая, когда у нас EventArgs.Empty, чтобы не передавать их явно. Тогда код сократится до Changed.SafeRaise(this), но я не буду рекомендовать такой подход, т.к. для других членов вашей команды это может быть не так явно, как передача пустого аргумента.
Тонкость №4 — что не так со стандартной реализацией?
Бонус: попытка Microsoft сделать контравариантные события
- 1. Вы не должны испытывать страх перед изучением. Пожалуйста, читайте медленно и вдумчиво.
- 2. Вы должны понимать классы и методы.
- 3. Вы должны
знатьпонимать, что есть делегаты. Хотя, Вы можете попытаться понять их в ходе чтения статьи.
Итак, Событие, это ситуация, при возникновении которой, произойдут некоторые действия. Само событие имеет определенную структуру.
Предположим, что стоит такая задача: определено три класса. Первый класс будет считать до 100, используя цикл. Два других класса будут ждать, когда в первом классе счетчик досчитает, например, до 71, и после этого каждый выведет в консоль фразу «Пора действовать, ведь уже 71!». Проще говоря, при обнаружении значения 71, вызовутся по методу, соответственно для каждого класса. Разложим все по полкам.
1. Моделирование ситуации.
Подготовим эти три простейших класса, оставив точку входа в программу main нетронутой.
Класс ClassCounter и его метод Count() в котором будет производится счет. (В коде я опускаю пространства имен namespace, ибо это ясно, как день).
Два других класса (имена им Handler_I и Handler_II), которые должны реагировать на возникновение события методами public void Message(). У каждого по методу, как и договаривались.
Напомню, когда счетчик будет считать до 100 и достигнет 71, должны сработать методы Message() для классов Handler_I и Handler_II.
Теперь вернемся к классу ClassCounter и создадим счетчик при помощи цикла for c переменной-счетчиком int i.
2. Оформление события.
Абстрагируемся от программирования. Событие, которое мы хотим создать, будет представлять фразу "… счетчик считает. И как только он будет равен 71, должны выполниться действия". Значит, нам необходимо условие «как только он будет равен 71». Представим его при помощи условного оператора if.
Конструируем событие event. Определяем по методам, которые должны сработать при i=71 их сигнатуру (или прототип).
Сигнатура метода — это так называемая спецификация (или простыми словами «шаблон») какого-л. метода или методов. Представляет собой сочетание названия типа, который метод возвращает, плюс название типов входящих параметров (по порядку! порядок очень важен.)
Например, метод int NewMethod(int x, char y) будет иметь сигнатуру int (int, char), а метод void NewMethod() — void (void).
Как толкует MSDN, события (event) основаны на делегатах (delegate), а делегат, говоря очень простым языком — «переменная, хранящая ссылку на метод». Как Вы уже поняли, т.к. наше событие будет ссылаться на два метода void Message(), мы должны определить сигнатуру этих методов, и составить на основе этой сигнатуры делегат. Сигнатура выглядит так: void (void).
Определяем делегат (назовем его MethodContainer):
Далее, мы создаем событие при помощи ключевого слова event и связываем его с этим делегатом (MethodContainer), а, следовательно, c методами, имеющими сигнатуру void (void). Событие должно быть public, т.к. его должны использовать разные классы, которым нужно как-то отреагировать (классы Handler_I и Handler_II).
Событие имеет синтаксис: public event ;
Название делегата — это имя делегата, на который «ссылаются» наши методы.
Теперь запустим наше событие onCount, в условии когда i=71:
Все. Событие создано. Методы, которые вызовет это событие, определены по сигнатурам и на основе их создан делегат. Событие, в свою очередь, создано на основе делегата. Пора показать событию onCount, какие же все-таки методы должны сработать (мы ведь указали только их сигнатуру).
Вернемся в точку входа программы main и создадим экземпляр класса ClassCounter. А также создадим по экземпляру классов, которые должны запуститься. (Они должны быть public).
Теперь укажем событию onCount, методы, которые должны запуститься.
Происходит это следующим образом: . += . .
Никаких скобочек после метода! Мы же не вызываем его, а просто указываем его название.
Проверка.
Теперь осталось запустить счетчик класса ClassCounter и подождать, пока i станет равным 71. Как только i=71, запустится событие onCount по делегату MethodContainer, который (в свою очередь) запустит методы Message(), которые были подписаны на событие.
Результат:
Пора действовать, ведь уже 71!
Точно, уже 71!
Заключение.
- 1. Определите условие возникновения события и методы которые должны сработать.
- 2. Определите сигнатуру этих методов и создайте делегат на основе этой сигнатуры.
- 3. Создайте общедоступное событие на основе этого делегата и вызовите, когда условие сработает.
- 4. Обязательно (где-угодно) подпишитесь на это событие теми методами, которые должны сработать и сигнатуры которых подходят к делегату.
Преимущество Событий очевидно: классу-издателю, генерирующему событие не нужно знать, сколько классов-подписчиков подпишется или отпишется. Он создал событие для определенных методов, ограничив их делегатом по определенной сигнатуре.
События широко используются для составления собственных компонентов управления (кнопок, панелей, и т.д.).
У самых маленьких может возникнуть вопрос: что делать, если методы, которые должны сработать имеют входящий параметр (а то и не один!)?
Ответ: Все дело в делегате, на котором базируется событие. А точнее сигнатура подходящих для делегата методов. Когда Вы сконструируете делегат, «принимающий» метод с параметром, то (!) при запуске событие запросит этот параметр. Естественно, параметр может быть чем угодно.
Многие разработчики утверждают (и я с ними согласен), что главная проблема «недопонимания» событий — их специфическая область применения, а вследствие — мало доступных примеров. Ну и не забывайте о практике.
P.S. Если вы не ни разу не использовали делегаты, лучше попробуйте потренироваться на делегатах, а затем попытайтесь понять эту статью.
Я надеюсь, что внес небольшое понимание в эту непростую тему. Успехов!
От переводчика
Несмотря на немаленький размер статьи, нельзя не согласиться, что тема делегатов и событий намного более обширна, сложна и многогранна. Однако гипотетическая статья, полностью описывающая делегаты и события, имела бы размер, сходный с размером средней книги. Поэтому я привожу ссылки на наиболее полезные статьи по теме, причём такие, которые как можно более гармонично дополняют именно эту статью.
Алексей Дубовцев. Делегаты и события (RSDN).
Хотя статья не новая (датируется 2006 годом) и рассматривает лишь основы делегатов и событий, уровень «рассмотрения основ» намного глубже: здесь и более пристальное рассмотрение типа MulticastDelegate, особенно в плане комбинированных делегатов, и описание принципа работы на уровне MSIL, и описание класса EventHandlerList, и многое другое. В общем, если вы хотите рассмотреть основы делегатов и событий на более глубоком уровне, то данная статья определённо для вас.
Обработчики статических и динамических событий
Обработчики событий
Для обработки события в приемнике события необходимо определить метод обработчика события. Этот метод должен соответствовать сигнатуре делегата обрабатываемого события. В обработчике событий выполняются действия, необходимые при возникновении события, например сбор данных, введенных пользователем при нажатии кнопки. Чтобы получать уведомления при возникновении события, метод обработчика события должен быть подписан на событие.
В следующем примере показан метод обработчика события c_ThresholdReached , который соответствует сигнатуре делегата EventHandler. Метод подписывается на событие ThresholdReached .
Потокобезопасные события
Если вы хотите, чтобы ваш код был истинно потокобезопасным, таким, что когда вы вызываете событие, то всегда используете наиболее актуальное значение переменной делегата, а также таким, что вы можете убедиться, что операции add/remove не мешают одна другой, то для достижения такой «железобетонной» потокобезопасности вам необходимо писать тело операций add/remove самому. Пример ниже:
Вы можете использовать единую блокировку для всех ваших событий, и даже использовать эту блокировку для чего-либо ещё — это уже зависит от конкретной ситуации. Обратите внимание, что вам нужно «записать» текущее значение в локальную переменную внутри блокировки (для того, чтобы получить самое актуальное значение), а затем проверить это значение на null и выполнить вне блокировки: удерживание блокировки во время вызова события является очень плохой идеей, легко приводящей к взаимоблокировке. Чтобы объяснить это, представьте, что некий обработчик событий должен дождаться, пока другой поток выполнит какую-то свою работу, и если во время неё этот другой поток вызовет операцию add/remove для вашего события, то вы получите взаимоблокировку.
Вышеприведённый код работает корректно потому, что как только локальной переменной handler будет присвоено значение someEvent, то значение handler уже не изменится даже в том случае, если изменится сам someEvent . Если все обработчики событий отпишутся от события, то список вызовов будет пуст, someEvent станет null, но handler будет хранить своё значение, которое будет таким, каковым оно было на момент присвоения. На самом деле, экземпляры делегатов являются неизменяемыми (immutable), поэтому любые подписчики, подписавшиеся между присваиванием ( handler = someEvent ) и вызовом события ( handler (this, e); ), будут проигнорированы.
Если на момент вызова метода OnSomeEvent переменная делегата someEvent не содержит списка экземпляров делегатов (вследствие того, что они не были добавлены через метод add или же были удалены через метод remove), то значение этой переменной будет null, и чтобы избежать её вызова с таким значением, и была добавлена проверка на null. Подобную ситуацию можно решить и другим путём. Можно создать экземпляр делегата-заглушку (no-op), который будет привязан к переменной «по умолчанию» и не будет удаляться. В этом случае в методе OnSomeEvent нужно просто получить и вызвать значение переменной делегата. Если «реальные» экземпляры делегатов так и не были добавлены, то будет просто-напросто вызвана заглушка.
Экземпляры делегатов: другие методы
В первом примере, представленном ниже 2↓ , нет обратных вызовов, здесь просто используются BeginInvoke и EndInvoke в одном потоке. Такой шаблон кода иногда полезен, когда один поток используется для синхронных в целом операций, но вместе с тем содержит элементы, которые могут быть выполнены параллельно. Ради простоты кода все методы в примере статические, но вы, конечно же, можете использовать «асинхронные» делегаты вместе с экземплярными методы, и на практике это будет происходить даже чаще. Метод EndInvoke возвращает то значение, которое возвращается в результате вызова экземпляра делегата. Если во время вызова экземпляра делегата возникнет исключение, то это исключение выбросит и EndInvoke .
Вызовы метода Thread.Sleep вставлены только ради того, чтобы продемонстрировать, что методы CountCharacters и Parse действительно выполняются параллельно с основным потоком. Сон в CountCharacters в 2 секунды достаточно большой для того, чтобы принудить пул потоков выполнить задачи в других потоках — пул потоков сериализует запросы, которые не требуют много времени для выполнения, чтобы таким образом избежать чрезмерного создания новых потоков (создание новых потоков является относительно ресурсоёмкой операцией). «Усыпляя» поток на долгое время, мы таким образом имитируем «тяжелую», требующую много времени на выполнение задачу. А вот и вывод нашей программы:
Если процесс выполнения делегата в стороннем потоке ещё не завершился, то вызов метода EndInvoke в основном потоке будет иметь схожий эффект с вызовом Thread.Join для вручную создаваемых потоков — основной поток будет ждать, пока завершится задача в стороннем потоке. Значение IAsyncResult , которое возвращается методом BeginInvoke и передаётся на вход EndInvoke , может использоваться для передачи состояния из BeginInvoke (через последний параметр — Object state ), однако необходимость в такой передаче при использовании делегатов возникает не часто.
На этот раз почти вся работа выполняется в потоках из пула потоков. Основной поток просто инициирует асинхронные задачи и «засыпает» до тех пор, пока все эти задачи не выполнятся. Все потоки из пула потоков являются фоновыми (background) потоками, которые не могут «удерживать» приложение (т.е. они не могут предотвратить его закрытие), и чтобы приложение не завершилось до того, как в фоновых потоках завершится работа делегатов, мы и применили вызов Thread.Sleep(3000) в основном потоке — можно надеяться, что 3-х секунд хватит для выполнения и завершения делегатов. Вы можете это проверить, закомментировав строчку Thread.Sleep(3000) — программа завершится почти мгновенно после запуска.
Результат работы нашей программы представлен ниже. Обратите внимание на порядок вывода результатов на консоль — результат работы парсера появился до результата работы счётчика, так как среда не гарантирует сохранение порядка при вызове EndInvoke . В предыдущем примере парсинг завершался значительно быстрее (100 мс), чем счётчик (2 сек), однако основной поток ждал их обоих, чтобы вывести прежде всего результат счётчика, а лишь потом парсера.
Данные событий
Класс EventArgs является базовым типом для всех классов данных событий. Класс EventArgs используется также, если событие не содержит связанных данных. При создании события, которое лишь уведомляет другие классы о том, что что-то произошло, и не передает никаких данных, используйте класс EventArgs в качестве второго параметра в делегате. Если данные не предоставляются, можно передать значение EventArgs.Empty. Делегат EventHandler содержит класс EventArgs в качестве параметра.
В следующем примере показан класс данных события с именем ThresholdReachedEventArgs . Он содержит свойства, относящиеся только к вызываемому событию.
Field-like события
Итак, что происходит, когда вы в коде ссылаетесь на MyEvent ? Внутри тела самого типа (включая вложенные типы) компилятор генерирует код, который ссылается на переменную делегата ( _myEvent в примере выше). Во всех остальных контекстах компилятор генерирует код, который ссылается на событие.
Примечания
2. Прим. перев. Этот и следующий примеры кода, как и их вывод на консоль, были немного изменены по сравнению с оригинальными примерами от Дж. Скита. Помимо перевода, я добавил вывод идентификаторов потоков, чтобы было чётко видно, какой код в каком потоке исполняется.
Делегаты
Делегат — это тип, содержащий ссылку на метод. Делегат объявляется с сигнатурой, указывающей тип возвращаемого значения и параметры для методов, на которые он ссылается, и может содержать ссылки только на методы, соответствующие его сигнатуре. Таким образом, делегат эквивалентен указателю на строго типизированную функцию или обратному вызову. Объявления делегата достаточно для определения класса делегата.
Заключение
Делегаты предоставляют простой способ вызова методов с учётом экземпляров объектов и с возможностью передачи неких данных. Они являются основой для событий, которые сами по себе являются эффективным механизмом для добавления и удаления обработчиков, которые будут вызваны в соответствующее время.
События
В следующем примере показан способ объявления события ThresholdReached . Событие связано с делегатом EventHandler и возникает в методе OnThresholdReached .
Экземпляры делегатов: основы
Теперь мы знаем, как объявляется тип делегата и что он содержит, так что давайте взглянем, как можно создать экземпляр делегата, и что с ним можно сделать.
Создание экземпляров делегатов
Как говорилось ранее, каждый экземпляр делегата обязательно содержит ссылку на целевой метод, который может быть вызван через этот экземпляр делегата, и ссылку на экземпляр объекта (класса или структуры), в котором объявлен целевой метод. Если целевой метод является статическим, то, естественно, ссылка на экземпляр отсутствует. CLR поддерживает и другие, немного различные формы делегатов, где первый аргумент, передаваемый в статический метод, хранится в экземпляре делегата, или же ссылка на целевой экземплярный метод передаётся как аргумент при вызове метода. Более подробно об этом можно прочитать в документации к System.Delegate на MSDN, однако сейчас, на данном этапе, эти дополнительные сведения не существенны.
Конструктор делегата, о котором мы говорили ранее, имеет два параметра — ссылку на вызываемый метод типа System.IntPtr (в документации MSDN этот параметр называется method) и ссылку на экземпляр объекта типа System.Object (в документации MSDN этот параметр называется target), которая принимает значение null, если метод, указанный в параметре method, является статическим.
Необходимо сделать важное замечание: экземпляры делегатов могут ссылаться на методы и экземпляры объектов, которые будут невидимыми (вне области видимости) по отношению к тому месту в коде, где будет произведён вызов экземпляра делегата. Например, при создании экземпляра делегата может быть использован приватный (private) метод, а потом этот экземпляр делегата может быть возвращён из другого, публичного (public) метода или свойства. С другой стороны, экземпляр объекта, указанный при создании экземпляра делегата, может быть объектом, который при вызове будет неизвестным по отношению к тому объекту, в котором был совершен вызов. Важно то, что и метод, и экземпляр объекта должны быть доступны (находиться в области видимости) на момент создания экземпляра делегата. Другими словами, если (и только если) в коде вы можете создать экземпляр определённого объекта и вызвать определённый метод из этого экземпляра, то вы можете использовать этот метод и экземпляр объекта для создания экземпляра делегата. А вот во время вызова ранее созданного экземпляра делегата права доступа и область видимости игнорируются. Кстати, о вызовах…
Вызов экземпляров делегатов
Экземпляры делегатов вызываются таким же образом, как вызываются обычные методы. К примеру, вызов экземпляра делегата d1, тип которого определён в самом верху как delegate string FirstDelegate (int x) , будет следующим:
Метод, ссылку на который хранит экземпляр делегата, вызывается «в рамках» (или «в контексте», если другими словами) экземпляра объекта, если такой есть, после чего возвращается результат. Написание полноценной программы, демонстрирующей работу делегатов, и при этом компактной, не содержащей «лишнего» кода, является непростой задачей. Тем не менее, ниже приведена подобная программа, содержащая один статический и один экземплярный метод. Вызов DelegateTest.StaticMethod эквивалентен вызову StaticMethod — я включил название класса, чтобы сделать пример более понимаемым.
Комбинирование делегатов
Делегаты могут комбинироваться (объединяться и вычитаться) таким образом, что когда вы вызываете один экземпляр делегата, то вызывается целый набор методов, причём эти методы могут быть из различных экземпляров различных классов. Когда я раньше говорил, что экземпляр делегата хранит ссылки на метод и на экземпляр объекта, я немного упрощал. Это справедливо для тех экземпляров делегатов, которые представляют один метод. Для ясности в дальнейшем я буду называть такие экземпляры делегатов «простыми делегатами» (simple delegate). В противовес им, существуют экземпляры делегатов, которые фактически являются списками простых делегатов, все из которых основываются на одном типе делегата (т.е. имеют одинаковую сигнатуру методов, на которые ссылаются). Такие экземпляры делегатов я буду называть «комбинированными делегатами» (combined delegate). Несколько комбинированных делегатов могут быть скомбинированы между собой, фактически становясь одним большим списком простых делегатов. Список простых делегатов в комбинированном делегате называется «списком вызовов» или «списком действий» (invocation list). Т.о., список вызовов — это список пар ссылок на методы и экземпляры объектов, которые (пары) расположены в порядке вызова.
Важно знать, что экземпляры делегатов всегда неизменяемы (immutable). Каждый раз при объединении экземпляров делегатов (а также при вычитании – это мы рассмотрим чуть ниже) создаётся новый комбинированный делегат. В точности, как и со строками: если вы применяете String.PadLeft к экземпляру строки, то метод не изменяет этот экземпляр, а возвращает новый экземпляр с проделанными изменениями.
Операторы сложения и вычитания всегда работают как часть операции присваивания d1 += d2 , которая полностью эквивалента выражению d1 = d1+d2 ; то же самое для вычитания. Снова-таки, напоминаю, что экземпляры делегатов, участвующие в сложении и вычитании, не изменяются в процессе операции; в данном примере переменная d1 просто сменит ссылку на новосозданный комбинированный делегат, состоящий из «старого» d1 и d2.
Обратите внимание, что добавление и удаление делегатов происходит с конца списка, поэтому последовательность вызовов x += y; x -= y; эквивалентна пустой операции (переменная x будет содержать неизменный список подписчиков, прим. перев.).
Если сигнатура типа делегата объявлена такой, что возвращает значение (т.е. возвращаемое значение не является void) и «на основе» этого типа создан комбинированный экземпляр делегата, то при его вызове в переменную будет записано возвращаемое значение, «предоставленное» последним простым делегатом в списке вызовов комбинированного делегата.
Если есть комбинированный делегат (содержащий список вызовов, состоящий из множества простых делегатов), и при его вызове в каком-то простом делегате произойдёт исключение, то в этом месте вызов комбинированного делегата прекратится, исключение будет проброшено, и все остальные простые делегаты из списка вызовов так никогда и не будут вызваны.
События
Перво-наперво: события (event) не являются экземплярами делегатов. А теперь снова:
События — это НЕ экземпляры делегатов.
Я пришел к выводу, что самый лучший способ понять события, это думать о них как о «как бы» свойствах (properties). Свойства, хотя и выглядят «типа как» поля (fields), на самом деле ими определённо не являются — вы можете создать свойства, которые вообще никак не используют поля. Подобным образом ведут себя и события — хотя и выглядят как экземпляры делегатов в плане операций добавления и вычитания, но на самом деле не являются ими.
Моменты, когда приходится игнорировать полученное значение value , возникают довольно редко. И хотя случаи, когда мы можем игнорировать передаваемое таким образом значение, крайне редки, бывают случаи, когда нам не подойдет использование простой переменной делегата для содержания подписчиков. Например, если класс содержит множество событий, но подписчики будут использовать лишь некоторый из них, мы можем создать ассоциативный массив, в качестве ключа которой будет использовать описание событие, а в качестве значения — делегат с его подписчиками. Именно эта техника используется в Windows Forms — т.е. класс может содержать огромное количество событий без напрасного использования памяти под переменные, которые в большинстве случаев будут равными null.
Создание нескольких событий
Свойства событий состоят из объявлений событий и методов доступа к событиям. Методы доступа к событиям — это определяемые пользователем методы, добавляющие или удаляющие экземпляры делегата события из структуры данных хранения. Обратите внимание, что использование свойств события снижает быстродействие по сравнению с полями события, поскольку перед вызовом каждого делегата события его необходимо извлечь. Необходимо найти компромисс между памятью и скоростью. Если ваш класс определяет много событий, которые вызываются нечасто, необходимо реализовать свойства событий. Дополнительные сведения см. в разделе Практическое руководство. Обработка нескольких событий с помощью их свойств.
Событие — это действие, требующее реагирования или "обработки" в коде. События могут генерироваться действиями пользователя (например, нажатием кнопки мыши или клавиши на клавиатуре), программным кодом или системой.
Приложения, управляемые событиями, выполняют код в ответ на событие. Каждая форма и элемент управления имеют предопределенный набор событий, который можно запрограммировать. Если возникает такое событие, а в соответствующем обработчике событий имеется код, этот код выполняется.
Типы порождаемых объектом событий могут варьироваться, но многие их них стандартны для большинства элементов управления. Например, большинство объектов обработают событие Click. Если пользователь откроет форму, в форме сработает код обработчика события Click.
Многие события возникают вместе с другими событиями. Например, при возникновении события DoubleClick возникают также события MouseDown, MouseUp и Click.
Сведения о том, как порождать и использовать события см. в разделе События.
Какой в этом смысл?
- Публичная переменная (поле) с типом делегата.
- Приватная переменная (поле) с типом делегата с оболочкой в виде публичного свойства.
- Приватная переменная (поле) с типом делегата с публичными методами AddXXXHandler и RemoveXXXHandler.
Читайте также: