Библиотека Интернет Индустрии I2R.ru |
|||
|
Использование делегатов и событийНеотъемлемой частью в практике программирования стало использование обратных вызовов (callback) и уведомлений (notifications). Основанные на них приемы нашли широкое применение в написании кода. К тому же в последнее время стало все больше распространяется программирование с использованием событий (заметим, что события реализуются с помощью уведомлений). Для реализации обратных вызовов и уведомлений в языках C и C++ используют указатели на функции. Такой подход несет в себе некоторые недостатки, так как не предоставляет типовой защищенности, что может привести к появлению ошибок связанных с преобразованием типов, которые не выявляются на стадии компиляции. Поскольку типовая защищенность - одно из достоинств C#, в этом языке Вы не можете объявить указатель на функцию. Для реализации вышеупомянутых действий (обратных вызовов и уведомлений) в C# используются делегаты. Делегат представляет собой подобие указателя на функцию. Он сохраняет его (указателя на функцию) преимущества, при этом избавляя от его недостатков. Использование делегатов позволяет добиться тех же результатов, что и при использования указателей на функции в C/C++, не теряя типовой защищенности. Данная статься рассматривает использование делегатов и событий в программах на C#. Особое внимание уделяется их внутренней структуре, а также реализации событийной модели в CLR (.NET Runtime). Объявление делегатов Поскольку CLR объектно-ориентированная среда, в ней все так или иначе связано с классами и объектами. В полной мере это относится и к делегатам. Делегаты в CLR реализованы в виде классов, производных от базового класса делегата: System.Delegate или System.MulticastDelegate. Строго говоря, делегат - это класс, содержащий данные о сигнатуре метода. Экземпляр делегата (delegae instance) - объект, который позволяет привязаться к конкретному методу, соответствующему определенной сигнатуре (подобно объявлению указателя на функцию в C/C++). Как и указатель на функцию делегат может быть передан в виде аргумента. Рассмотрим действия, которые надо проделать, чтобы воспользоваться делегатами. Для начала необходимо объявить делегат. Для объявления делегатов в C# используется ключевое слово delegate. То есть концепция делегатов непосредственно поддерживается синтаксисом языка. Ниже приведен пример объявления делегата:
Все что мы получили от вышеприведенного объявления - это новый тип (класс). Это и следовало было ожидать, так как класс и является пользовательским типом данных. Что определяет этот класс? Данный класс определяет форму метода - его сигнатуру. В сигнатуру метода входит тип возвращаемого значения и список аргументов. В нашем случае, метод должен возвращать строку и принимать один целочисленный аргумент. Так как делегат является классом, он может быть объявлен как в пространстве имен, так и в классе. Оба ниже приведенных листинга содержат корректные объявления:
Во втором случае определяется вложенный класс. В определении делегата (перед ключевым словом delegate) могут быть указаны модификаторы доступа, их смысл идентичен смыслу модификаторов доступа обычного класса. Делегат определен. Далее необходимо создать его экземпляр. Экземпляр делегата является ни чем иным как объектом, и создается подобно экземпляру любого класса:
В этом фрагменте кода MyDelegate - имя класса, а dg - имя объекта. Как и любой объект в C# перед использованием экземпляр делегата необходимо инициализировать. И в этом случае экземпляр делегата инициализируется как и экземпляр любого класса, при помощи оператора new.
Однако здесь есть одно очень существенное отличие. Как видно из вышеприведенного фрагмента кода, конструктору делегата передается идентификатор метода (имя), на который будет указывать экземпляр делегата. Отличие заключается в том, что при попытке передать идентификатор метода конструктору обычного класса, независимо от типа аргумента конструктора, будет выдано следующее сообщение об ошибке:
Данный факт объясняется тем, что концепция делегатов поддерживается синтаксисом C#. Передача имени метода допустима только конструкторам делегатов. Как и для обычных классов, при объявлении экземпляр делегата можно инициализировать:
Экземпляр делегата может быть привязан, как к статическому так и к нестатическому методу класса. В случае со статическим методом, перед именем метода указывается имя класса. Если имя класса опущено, подразумевается класс, в реализации которого встречается инициализация экземпляра делегата. В случае с нестатическим методом, перед его именем указывается имя объекта Если имя объекта опущено, предполагается, что используется текущий экземпляр (this):
Экземпляр делегата объявлен и инициализирован, все готово для его использования. Вызов метода, на который указывает делегат, напоминает вызов обычного метода, но в этом случае вместо имени метода указывается имя экземпляра делегата:
В круглых скобках указывается список аргументов. И естественно, поскольку наш метод возвращает строковое значение, мы можем получить его:
А теперь рассмотрим простой пример использования делегата:
После первого знакомства с делегатами можно двигаться дальше. Ниже будут рассмотрены типы делегатов. Типы делегатов Существует два типа делегатов: одиночные (singlecast delegate) и комбинированные (multicast delegate). Пример использования одиночного делегата был рассмотрен выше. С помощью одиночного делегата за один раз может быть вызван единственный метод, в то время как с помощью комбинированного делегата можно вызвать либо один, либо несколько методов одновременно. Одиночный делегат мы умеем определять, а как определить комбинированный делегат? Различие заключается в сигнатуре делегата, а точнее - в типе возвращаемого значения. Если делегат объявлен с типом возвращаемого значения void, он является комбинированным. Это вполне логично, поскольку комбинированные делегаты одновременно могут вызвать более одного метода, в то время как сам вызов может возвращать не более одного значения. Комбинированные делегаты наследуют реализацию от класса System.MulticastDelegate. В случае объявления с типом отличным от void делегат является одиночным. Выше был определен делегат с типом возвращаемого значения string. Одиночные делегаты являются производными от System.Delegate. Для подтверждения можно воспользоваться утилитой IL Disassembler, входящую в состав .NET SDK. Ниже показано окно IL Disassembler изображающее откомпилированный пример из первого пункта. В этом примере создается одиночный делегат MyDelegate, как видно он наследует реализацию от System.Delegate. Изменим пример следующим образом:
Мы изменили объявление делегата с:
на
Откомпилируем и откроем исполняемый файл с помощью IL Disassembler: Как вы видите, делегат с типом возвращаемого значения void расширяет класс System.MulticastDelegate, то есть является комбинированным делегатом. Внутренняя структура делегатов Мы рассмотрели простейший пример использования делегатов. Теперь постараемся разобраться в их внутренней структуре. Сначала рассмотрим одиночные делегаты, потому как комбинированные делегаты являются их расширением. А немного позже мы разберемся с использованием комбинированных делегатов. Все что касается одиночных делегатов применимо и к комбинированным. Убедится в этом можно посмотрев на иерархию наследования классов System.Delegate и System.MulticastDelegate. Также приведем объявление обоих классов:
и
Оба класса являются абстрактными. Их реализация находится в сборке mscorlib.dll пространства имен System. Одиночные делегаты Как Вы помните, одиночные делегаты являются классами, производными от System.Delegate. Чтобы ознакомится с этим классом откройте документацию на него. Среди методов и свойств данного класса сейчас нас особо интересуют два свойства: Target и Method. Вот их объявления:
Если делегат указывает на нестатический метод, то при его инициализации свойству Target присваивается ссылка на экземпляр конкретного объекта - контекст объекта. В случае же со статическим методом его значение null. Второе свойство - Method содержит данные о методе. В отличии от первого свойства его значение никогда не равно null. Оба свойства являются свойствами только для чтения: Вы не можете непосредственно модифицировать их значение. Значение данных свойств устанавливается при инициализации делегата. Продолжая изучение класса System.Delegate, рассмотрим его конструктор. Класс System.Delegate имеет два конструктора, приведем их объявление:
Данный конструктор предназначен для инициализации делегатов, указывающих на статический метод. Первый аргумент определяет тип (класс), в котором находится статический метод. Второй аргумент задает строку с именем самого метода.
А этот конструктор предназначен для инициализации делегатов, указывающих на нестатический метод. Первый аргумент определяет контекст объекта. Смысл второго аргумента аналогичен смыслу второго аргумента в первом конструкторе. Однако ни один из этих конструкторов Вам не понадобится при программировании на C#, поскольку компилятор сам генерирует производный класс делегата и вызывает конструктор, скрывая от Вас детали реализации, тем самым, предоставляя более простой синтаксис объявления делегатов. Теперь рассмотрим, каким образом происходит вызов метода на который указывает делегат. Для этого снова обратитесь к примеру первого пункта (листинг 3). Встречая следующую строку,
компилятор распознает, что dg это экземпляр делегата. Вызов происходит посредством рефлексии. Рефлексия - это замечательный механизм, позволяющий динамически работать с типами, читать информацию о типе и вызывать любые методы класса во время выполнения. Как Вы помните, каждый делегат имеет два свойства: Method и Target. Свойство Method имеет тип MethodInfo. В классе MethodInfo существует метод Invoke (вызвать), позволяющий динамически вызвать метод, данные о котором хранятся в объекте. Компилятор использует значение свойств Target и Method делегата и генерирует код для вызова Invoke. Если Вы не знакомы с рефлексией, обязательно с ней познакомьтесь, в будущем Вы еще неоднократно с ней встретитесь. Только что Вы увидели наглядный пример ее применения. Концепция атрибутов также основана на использовании рефлексии. Комбинированные делегаты Рассмотрим следующий пример кода:
Это простейший пример использования комбинированного делегата. Однако в этом случае он идентичен применению одиночного делегата, потому как вызывается единственный метод. А как Вы помните, комбинированные делегаты способны ссылаться на более чем один метод. Чтобы создать комбинированный делегат, который указывает на более чем один метод, необходимо комбинировать делегаты. Для этого в классе System.Delegate существует статический метод Combine:
Рассмотрим пример использования этого метода:
В приведенном примере сначала создаются два делегата: mcdg1 и mcdg2. Затем с помощью метода Combine они комбинируются. В результате мы получаем комбинированный делегат mcdg3, результатом вызова которого будет вызов обоих методов: делегата mcdg1 и mcdg2. Чтобы за один вызов Combine скомбинировать более двух делегатов можно воспользоваться перегруженной версией этого метода: Вот пример его использования:
Как Вы наверное заметили, возвращаемое значение метода Combine необходимо явно преобразовать к соответствующему типу делегата, так как метод возвращает значение с типом Delegate:
Структура комбинированного делегата С точки зрения реализации, комбинированный делегат лишь немногим отличается от одиночного. Различие заключается в следующем: класс System.MulticastDelegate содержит дополнительное скрытое поле _prev, содержащее ссылку на другой комбинированный делегат: А сама сущность - комбинированный делегат представляется в виде связанного списка делегатов с типом возвращаемого значением void. Действительно, одиночный и комбинированный делегаты имеют очень много общего, и Вы вправе использовать комбинированные делегаты (с типом void) как одиночные (листинг 5). Дополнительное преимущества комбинированных делегатов в том, что Вы можете объединить их в связанный список, используя метод Combine, и одновременно вызывать более одного метода. Еще раз посмотрим на метод Combine:
Все что делает данный метод, это присваивает полю _prev делегата mcdg2 ссылку на делегат mcdg1, и возвращает ссылку на mcdg2. В этом случае mcdg2 будет являться головным элементом списка. Если же mcdg1 является связанным списком, то mcdg2 добавится в этот список. Рассмотрим еще пару полезных методов. В дополнение к методу Combine в классе System.Delegate также содержится метод Remove:
Он удаляет делегат, определенный в аргументе value из связанного списка делегатов, определенного аргументом source. Второй метод GetInvocationList:
возвращает массив одиночных делегатов. Если метод вызывается для комбинированного делегата, все элементы связанного списка возвращаются в виде одиночных делегатов в массиве. В случае одиночного делегата метод возвращается массив с единственным элементом. Обратные вызовы Здесь мы рассмотрим использование делегатов для реализации обратных вызовов. Рассмотрим простой пример:
Для начала нам необходимо определиться с сигнатурой callback-метода и объявить, соответствующий ей делегат (в нашем примере: CallBackMethod). Затем реализуем callback-метод: MyCallBackMethod, на который будет ссылаться наш делегат. Метод SimpleMethod осуществляет обратный вызов посредством делегата cbm, который передается в виде аргумента. Особенность callback-метода в том, что Вы передаете его в виде аргумента другому методу, а не вызываете его непосредственно. Событийная модель Событийная модель в CLR строиться на основе комбинированных делегатов. В событийной модели используется схема издатель/подписчик. Класс, предоставляющий события другим классом, способен уведомлять их о переходе в определенное состояние. В свою очередь желающие получить уведомление обязательно должны зарегистрироваться. Регистрация заключается в передаче классу-издателю делегата, указывающего на метод-обработчик события. Метод-обработчик находится в классе-подписчике и включает код, который должен быть выполнен в ответ на событие, произошедшее в классе-издателе.. Поскольку с делегатами мы разобрались, давайте попробуем предоставить собственную реализацию механизма, обеспечивающего работу событий.
Класс ClassWithEvents предоставляет возможность уведомлять подписчиков, посылая им некоторое сообщение. Каждое событие имеет тип, определяющий сигнатуру метода-обработчика события. Этот тип задается делегатом. В классе содержится всего три метода и одно поле. Для подписки на события класс предоставляет метод AddSubscriber, соответственно для отписки RemoveSubscriber. Метод AddSubscriber, заносит переданный делегат в связанный список. Поле subscribers - комбинированный делегат, содержит ссылку на головной элемент связанного списка делегатов подписчиков. Для генерации события используется метод RaiseEvent. В статическом методе Main класса TestApp приведен код, использующий класс-издатель. Он достаточно прост, потому не нуждается в комментариях. Не пожалейте времени и хорошо разберитесь в данном примере. Он дает четкое представление о механизме, обеспечивающем работу событий. Однако в повседневном программировании было бы довольно утомительно для каждого события предоставлять реализацию методов AddSubscriber и RemoveSubscriber. Поэтому C# поддерживает более простой синтаксис объявления событий. Специально для этого в С# введено ключевое слово event. Используя это ключевое слово, событие из предыдущего примера можно объявить следующим образом:
Ключевое слово event используется для определения экземпляра делегата, посредством которого будут осуществляться вызовы методов - обработчиков событий. Обратите внимания на синтаксис его использования. Вначале идет модификатор доступа события. Ключевое слово event говорит о том, что мы определяем поле - событие класса. Затем указывается тип делегата. Объявление класса делегата должно быть доступно в классе-издателе и в классе-подписчике. В заключении определяется имя события. Заметьте, если класс предоставляет событие другим классам оно должно быть открытым (public) А теперь модифицируем предыдущий пример, используя ключевое слово event
Класс ClassWithEvents стал проще: в нем нет методов AddSubscriber и RemoveSubscriber. Более того, также упростился синтаксис подписки и отписки на события. Вместо вызова методов можно использовать перегруженные операторы += и -= для подписки и отписки соответственно. Однако ответственность за генерацию события по-прежнему лежит на Вас: метод RaiseEvent не изменился. Преимущества данного подхода очевидны. А теперь посмотрим что же происходит, когда мы объявляем событие с ключевым словом event. Для этого снова воспользуемся IL Disassembler и откроем последний откомпилированный пример. Что мы видим? Компилятор объявил за нас скрытое поле-делегат: subscribers, который содержит список подписчиков (на рисунке поле выделено курсором). Также компилятор предоставил собственную реализацию методов add_subscribers и remove_subscribers, причем сигнатуры обоих методов идентичны нашей реализации. А теперь посмотрим на их код.
Дважды щелкните на имени метода add_subscribers, откроется окно, содержащее код реализации метода на IL (Intermediate Language):
Попробуем разобраться, что же этот метод делает и восстановить его исходный код. Исходный код можно восстановить так:
Как Вы видите, он отличается от нашей реализации. Оказывается, что метод add_subscribers просто вызывает, метод Combine. Проверка на null оказалась лишней, по видимому метод Combine об этом позаботился. Поэтому, если все же придется прибегнуть к использованию собственной реализации данных методов, используйте именно такое решение. Теперь попробуем восстановить исходный код remove_subscribers:
Здесь все аналогично:
Не забывайте использовать именно это решение. А теперь усовершенствуем нашу реализацию:
Да, все же иногда бывает полезно воспользоваться IL Disassembler! На этом с внутренней структурой делегатов и событий все. Далее мы кратко разберем использование событий на практике (при построении пользовательского интерфейса). Использование событий Для начала рассмотрим простейший пример использования событий, при программирования графического интерфейса:
Класс Button содержит событие Click (наследуемое от класса Control), имеющее тип EventHandler. Вот его объявление:
А вот он его тип:
реализации которого содержится в сборке mscorlib.dll пространства имен System. Как Вы видите, делегат является комбинированным, потому как все делегаты, определяющие тип события должны иметь тип void. Первый аргумент содержит ссылку на источник события - объект, генерирующий событие (в нашем случае это форма - MyForm). Второй аргумент имеет тип EventArgs. Он содержит аргументы, переданные от издателя подписчику. По правде говоря, в нашем случае он не содержит, какой либо полезной информации, поскольку обычно этот класс используется как базовый. Вы вправе определить свой класс, наследующий от EventArgs и внести собственные поля. В классе формы мы определили метод - обработчик события, соответствующий сигнатуре делегата EventHandler, с именем button1_Click. Затем используя перегруженный оператор += регистрируем обработчик события Click. Рассмотрим еще один пример использования событий - событий таймера. Для этого воспользуемся классом, находящемся в пространстве имен System.WinForms:
Класс содержит событие Tick:
которое возникает через определенный период времени, заданный в свойстве Interval (время задается в миллисекундах). Для включения и выключения таймера вызываются методы Start() и Stop() соответственно. После инициализации таймер находится в выключенном состоянии. Вот пример использования таймера:
На форме, расположены две кнопки: Timer On и Timer Off, которые включают и выключают таймер соответственно. При поступлении события от таймер на экран выводится диалоговое окно. В заключении приведем общий план применения событий, все что Вам необходимо:
Подробное рассмотрение конкретных событий выходит за рамки стати, поэтому на этом все. Особенности использования событий При использовании событий следует учитывать некоторые особенности. Событие может генерироваться только в методах того класса, в котором оно определено. Запрещается генерировать событие класса в производных или в других классах. Рассмотрим следующий пример:
Несмотря на размер данного примера он достаточно прост. Класс BaseEventClass содержит событие. В методе RaiseEvent находится код для его генерации. Производный от BaseEventClass класс - DerivedEventClass в методе UsingEvent пытается сгенерировать событие базового класса, однако это ему не удастся, поскольку при компиляции кода выводится следующее сообщение об ошибке:
Как вы видите этих сообщений два, потому как в коде, генерирующем событие:
два раза идентификатор события используется неверным образом. Тот же самый результат Вы получите, если попытаетесь поместить код, генерирующий событие в другом классе. Если же Вам необходимо генерировать событие из кода других классов (в том числе и производных), предоставьте открытый метод, подобный RaiseEvent. Однако это же сообщение нам говорит, что подписываться на события, можно из методов любых классов, в том числе и производных. Следующий код компилируется и работает:
Чтобы подписаться на событие базового класса мы пишем следующий код:
Как Вы видите, используется текущий экземпляр объекта (this), а метод DerivedEventClass_MyEvent не статический. Однако обычно в таких ситуациях используются виртуальные методы: пример - переопределение виртуального метода OnPaint, класса RichControl в классе, производном от Form. Почему возникает ошибка? Данный факт объяснить очень просто. Как вы помните, при определении события с именем MyEvent, компилятор определяет одноименное скрытое поле-делегат. Поскольку генерация события осуществляется посредством делегата, а делегат в свою очередь имеет модификатор доступа private, то в результате мы имеем доступ к делегату только из методов класса, в котором определено событие. Так как поддержка делегатов и событий в C# реализована на уровне синтаксиса, в нем существует специальное сообщение об ошибке CS0070, которое предоставляет более ясное пояснение, нежели ошибка нарушения доступа к скрытым членам класса, что наверняка сбило бы с толку программиста, не представляющего себе механизм работы событий, реализованный в CLR. Вы спросите: <А почему бы не сделать поле-делегат защищенным (protected)? Тогда возможно было бы генерировать событие базового класса в производных> Этого не было сделано разработчиками языка из соображений строгого следования принципам инкапсуляции. Действительно метод RaiseEvent может быть гораздо сложнее нашей реализации: события могут возникать при определенном состоянии объекта, проверка которого должна осуществляться в самом классе. Вот вам один из возможных вариантов ответа. Если Вас не устраивает данный факт (что маловероятно) - не пользуйтесь ключевым словом event, а прибегните к собственной реализации механизма, обеспечивающего работу событий (рассмотренный выше). Заключение Механизм событий CLR построен на основе делегатов, поэтому умение хорошо работать с делегатами значительно облегчит Вам жизнь в практике программирования для .NET. В данной статье достаточно подробно были рассмотрены основы использования делегатов и событий, а так же объяснены принципы их реализации в CLR. |
|
2000-2008 г. Все авторские права соблюдены. |
|