Предыдущие версии Visual Basic убедительно показали, что модель программирования, управляемая
событиями и основанная на применении объектов, повышает производительность труда
программиста. Стоило вам перетащить элемент на форму, как он начинал реагировать
на определенные события. Например, код процедуры события Button1_Click автоматически
выполнялся при нажатии кнопки с именем Button1.
Но, несмотря
на эффективность, модель, использованная в прежних версиях VB, была недостаточно
гибкой. В частности, в ней было трудно определять новые события, а написать
обработчик, который обрабатывает сразу несколько событий, было практически невозможно.
В VB .NET удобство и эффективность объединились с богатством возможностей. Обычно
используется синтаксис, очень близкий к синтаксису прежних версий VB, при этом
VB .NET берет на себя всю «черную работу». Если понадобится сделать
что-то нестандартное — VB .NET предоставит и такую возможность. Глава начинается
с описания модели обработки событий, похожей на аналогичную модель из предыдущих
версий VB (хотя и гораздо более мощной). Далее мы представим новую для VB концепцию
делегатов и покажем, как с их помощью в полной мере использовать возможности
платформы .NET по обработке событий, а также решать более общие задачи (например,
организовать обратный вызов функций).
Обработка
событий с точки зрения ООП
Поскольку
сущность объектно-ориентированного программирования в конечном счете сводится
к обмену сообщениями между взаимодействующими объектами, события должны занимать
определенное место в этой схеме. В каком-то отношении они нормально вписываются
в нее: объект-источник отправляет сообщение, которое указывает на возникновение
события.
Но при этом
возникает очевидная проблема: каким объектам следует отправлять сообщения? Оповещать
о каждом событии все объекты, в настоящий момент существующие в программе? Это
было бы слишком неэффективно. Для большийства
объектов событие не представляет ни малейшего интереса, а быстродействие станет
неприемлемо низким.
Вместо этого
VB .NET пытается ограничить число получателей события, для чего используется
модель «подписка/публикация». В этой модели объекты-приемники событий
регистрируют объекты-источники тех событий, которые представляют для них интерес.
На события от одного источника могут подписаться сразу несколько объектов-приемников.
О том, что источник инициировал событие, оповещаются только зарегистрированные
получатели.
Впрочем,
реализовать подобную схему не так просто. Какие сообщения должны передаваться
приемнику от источника? Как организовать их отправку? Что должно происходить
при получении сообщения? Как говорилось выше, взаимодействие между объектами
на базе обмена сообщениями строится на вызове методов класса-приемника. В конечном
счете обработка событий строится по тому же принципу, но при этом приходится
учитывать ряд дополнительных тонкостей.
Общий смысл
происходящего заключается в том, что при возникновении события объект-источник
вызывает заранее определенные функции объектов-приемников. Вызываемая функция
приемника регистрируется источником события одновременно с регистрацией объекта-приемника.
Такая схема называется оповещением посредством обратного вызова (callback
notification), потому что источник события вызывает метод приемника по заранее
известному ему адресу. На рис. 6.1 показан объект-«начальник» с
событием HighRating, при возникновении вызываются разные методы объектов-приемников.
Во второй половине этой главы будет рассказано, как это происходит в VB .NET.
Чтобы
регистрация прошла успешно, методы объекта-приемника должны иметь строго определенную
форму (например, обладать конкретной сигнатурой). По этой причине в некоторых
языках (например, в Java) механизм обратного вызова реализуется с применением
интерфейсов — это гарантирует наличие метода с правильной сигнатурой.
Рис.
6.1. Схема оповещения посредством обратного вызова
Передача
данных функциям, вызываемым в результате событий
Конечно,
вы можете определить собственную сигнатуру для методов объекта-приемника, вызываемых
источником, однако в .NET существует практически общепринятое правило, согласно
которому функции приемника передаются два параметра:
Пример приводился
ранее в главе 1. При размещении кнопки на форме генерировалась процедура события
Click:
Private Sub
Buttonl_Click(ByVal sender As System.Object.
ByValeAs System.EventArgs) Handles Button1.Click
End Sub
Параметры
имеют следующий смысл:
Традиционно
в VB источник (отправитель) события не идентифицировался в процедуре события.
Единственным исключением были массивы управляющих элементов, когда конкретный
элемент-отправитель выделялся из массива при помощи параметра-индекса. Смысл
дополнительной объектной переменной sender в обобщенной процедуре события VB
.NET становится очевидным, если вспомнить, что одна процедура может обрабатывать
несколько событий, поступающих от разных объектов. Попробуйте вызвать встроенный
метод ToString в приведенной выше процедуре события: MsgBox(sender.ToString)
Результат будет выглядеть так:
Таким образом,
процедура-обработчик может однозначно определить, какой объект был источником
события.
В данном
примере объект события е не представляет интереса, поскольку он не содержит
сколько-нибудь полезной информации о событии. С другой стороны, в некоторых
ситуациях он может пригодиться. Например, из объектной переменной
класса MouseEventArgs можно узнать, в какой точке был сделан щелчок мышью. В
общем случае программист определяет собственные классы событий, производные
от класса System. EventArgs и содержащие любую полезную информацию о событии
(о том, как это делается, будет рассказано ниже).
Также обратите
внимание на новое ключевое слово Hand! es в определении процедуры события. Как
нетрудно догадаться, это ключевое слово указывает, какие события обрабатываются
данной процедурой. Возможно, в данном примере ключевое слово Handl es выглядит
излишним, однако оно предоставляет программисту дополнительные возможности,
поскольку теперь обработчики события не обязаны обладать жестко заданными именами
(фиксируются только сигнатуры). Следовательно, одна процедура может обрабатывать
несколько событий, для чего в конец объявления процедуры включаются несколько
секций Handl es. Новый подход обладает большей гибкостью по сравнению с массивами
управляющих элементов, использовавшимися в прежних версиях VB (в VB .NET массивы
элементов не поддерживаются).
Хотя IDE
генерирует процедуры событий со стандартными именами, в VB .NET это уже не является
обязательным требованием. Если процедура имеет правильный набор параметров и
в ее заголовке присутствует ключевое слово Handles, эта процедура может использоваться
для обработки событий. Пример:
Private Sub MyClickProcedure(ByVal sender As System.Object,_
ByValeAs System.EventArgs)
Handles Buttonl.Click
Процедура
MyClickProcedure может обрабатывать событие Buttonl. Click благодаря
наличию правильных параметров. Она обрабатывает это событие, поскольку
в заголовке указано ключевое слово Handles. Главное новшество заключается в
явном указании обрабатываемых событий с ключевым словом Handles.
Рассмотрим
другой пример. Допустим, предыдущий фрагмент был приведен к следующему виду:
Private Sub
MyClickProcedureCByVal sender As System.Object._
ByVal e As System.EventArgs) Handles Buttonl.Click. Button2.Click._
mnuTHing.Click
Теперь одна
процедура обрабатывает события сразу от двух разных кнопок и команды
меню! В VB6 подобная универсальность была невозможна, поскольку в прежних версиях
VB обработчики событий вызывались по имени элемента. Надеемся, читатель согласится
с тем, что ключевое слово Handl es обладает значительно большим потенциалом,
чем массивы управляющих элементов.
Простейшее
инициирование событий
Давайте вернемся
к простому классу Empl oyee и подробно, шаг за шагом разберем все, что необходимо
сделать для определения и инициирования событий. Предположим, событие должно
инициироваться при попытке увеличения заработной платы более чем на 10 процентов
без ввода пароля. В главе 4 метод RaiseSalary выглядел так:
Public Overloads Sub RaiseSalary(ByVal percent As Decimal)
If percent >
LIMIT Then
' Операция запрещена
- Необходим пароль
Console.WriteLine("MUST
HAVE PASSWORD TO RAISE SALARY " & _
"MORE THAN
LIMIT!!!!") Else
m_Sa1ary =(1 + percent) * m_salary
End If
End Sub
Вместо выделенной
команды, выводящей текстовое сообщение на консоль, должно инициироваться событие.
Задача решается в несколько этапов. В простейшем случае в классе сначала объявляется
открытая переменная с ключевым словом Event, с указанием имени события
и его параметров. Например, следующая строка весьма близка к синтаксису VB6:
Public Event SalarySecurityEventdnessage as String) В этой строке объявляется
открытое событие с параметром строкового типа.
События
обычно объявляются с модификатором Public, никогда не возвращают значений и
могут получать параметры произвольного типа, кроме ParamArray.
После того
как переменная события будет определена, событие инициируется командой следующего
вида (впрочем, для того, чтобы событие действительно произошло, потребуются
еще кое-какие дополнительные действия):
RaiseEvent SalarySecurityEventC'MUST HAVE PASSWORD TO RAISE " & _
"Salary
MORE THAN LIMIT!! !!")
Однако из
этого не следует, что для любого события следует ограничиваться одним строковым
параметром. В соответствии с парадигмой программирования .NET в качестве параметров
любого события передается объект-источник и информация о событии, инкапсулированная
в объекте события. На первых порах вполне достаточно объявления вида
Public Event
SalarySecurityEvent(ByVal who As Employee, ByVale As system.EventArgs)
Событие инициируется следующей командой RaiseEvent:
RaiseEvent
SalarySecurityEvent(Me,New System.EventArgs())
Хотя
события обычно объявляются открытыми, это не является обязательным требованием
— событие может иметь любой модификатор уровня доступа. Закрытыми (Private)
объявляются события, представляющие интерес только для объектов этого класса,
а защищенные (Protected) события также могут обрабатываться объектами производных
классов. Допускается даже объявление общих (Shared) событий, которые, как и
общие члены классов, существуют на уровне класса в целом, а не его отдельных
членов (в частности, общие методы могут инициировать только общие события).
По сигнатуре
события приемник узнает, от какого источника поступило событие (в данном примере
это объект-работник, которому попытались неправильно повысить заработную плату);
сам объект передается в виде ключевого слова Me. Впрочем,
приведенное объявление не использует возможностей передачи данных в переменной
события е. Вскоре мы разработаем класс, производный от System. EventArgs, в
объектах которого будет содержаться строка предупреждения вместе с данными о
попытке повышения заработной платы.
Подключение
приемников к источнику
В нашем распоряжении
имеется весь код, необходимый для рассылки событий, но пока нет ни одного заинтересованного
получателя. Существует несколько способов, которыми класс может сообщить VB
.NET о своем желании получать события от другого класса. Простейший способ очень
похож на тот, который использовался в VB6: на уровне модуля (или класса) объявляется
переменная класса-приемника с ключевым словом WithEvents. Например, если включить
в класс следующую строку, не входящую ни в один из членов: Private WithEvents
anEmployee As Employee
объекты этого
класса становятся потенциальными приемниками событий, инициируемых классом Employee.
Обратите особое внимание на некоторые особенности этого объявления:
После включения
этой строки в программу объектная переменная anEmpl oyee может использоваться
всюду, где вас интересует событие SalarySecurityEvent. Как показано на рис.
6.2, IDE автоматически создает обработчик события с именем, построенным по схеме
А_В, для каждой объектной переменной, объявленной с ключевым словом Wi thEvents.
Чтобы вызвать автоматически сгенерированный «скелет» события, достаточно
выбрать его в раскрывающемся списке, как на рис. 6.2.
А теперь
давайте объединим все сказанное на практическом примере. Создайте консольное
приложение и включите следующий фрагмент в первый (стартовый) модуль:
Module Modulel
Private WithEvents
anEmployee As EmployeeWithEvents
Sub Main()
Dim tom As New EmployeeWithEvents("Tom". 100000)
anEmployee =
tom
Console.WriteLine(tom.TheName & "has salary " & tom.Salary)
anEmployee.RaiseSalary(0.2D) ' Суффикс D - признак типа Decimal
Console.WriteLinettom.TheName & "still has salary " & tom.Salary)
Console.WritelineC'Please press the Enter key")
Console.ReadLine()
End Sub End Module
Рис.
6.2. Автоматически сгенерированный код обработчика события
Теперь выберите
в раскрывающемся списке метод anEmployee_SalarySecurityEvent. Исходный текст
этого метода приведен ниже (для удобства чтения он разбит на несколько строк,
а ключевая секция Handles выделена жирным шрифтом):
Public Sub anEmployee_SalarySecur1tyEvent(ByVal
Sender As
Event_Handling_I.EmployeeWithEvents, ByValeAs System.EventArgs) Handles
anEmployee.SalarySecurityEverrt
End Sub
End Module
Обратите
внимание на символ подчеркивания, добавленный VB .NET между именем переменной
с ключевым словом WithEvents (anEmployee) и именем события (SalarySecurityEvent),
— с ним обработчик внешне почти не отличается от процедур событий в VB6.
Также обратите
внимание на идентификацию объекта Sender полным именем (в формате пространство_имен.
имя_класса). Наличие дополнительных символов подчеркивания в пространстве имен
объясняется тем, что пробелы в них не разрешены, поэтому VB .NET автоматически
преобразует имя решения «Event Handling 1» в «Event_Handling_l»
(рис. 6.3). Наконец, ключевое слово Handles сообщает исполнительной среде, какое
событие обрабатывается этой процедурой.
Рис.
6.3. Окно свойств решения с обработкой событий
Чтобы пример
стал более интересным, вместо простого вывода в консольное окно 'мы включим
в процедуру события команду вызова диалогового окна:
Public Sub anEmployee_SalarySecurityEvent(ByVal
Sender As
Event_Handling_I.EmployeeWithEvents.
ByVal e As System.EventArgs)
Handles anEmployee.SalarySecurityEvent
MsgBox(Sender.TheName
&"had an improper salary raise attempted!")
End Sub
От приемника
событий мы переходим к источнику. В класс Employee из главы 4 необходимо внести
два изменения, выделенные в следующем листинге жирным шрифтом:
Public Class EmployeeWithEvents
Private m_Name As String
Private m_Salary As Decimal
Private Const LIMIT As Decimal =0.1D
Public Event SalarySecurityEventCByVal Sender As
EmployeeWithEvents,ByVal e As EventArgs)
Public Sub NewCByVal
aName As String.
ByVal curSalary As Decimal)
m_Name = aName
m_Salary = curSalary
End Sub Readonly
Property TheName() As String
Get
Return m_Name
End Get
End Property
Readonly Property Salary() As Decimal s,
Get
Return m_Salary
End Get ' '
End Property
Public Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent >
LIMIT'Then
'Операция запрещена - необходим пароль
RaiseEvent SalarySecurityEventtMe, New System.EventArgs())
Else
m_Sa1ary = (1 + Percent) * m_Salary
End If
End Sub
Public Overloads
Sub RaiseSalary(ByVal Percent As Decimal.
ByVal Password As String)
If Password
= "special" Then
m_Salary = (1 + Percent) * m_Salary
End If
End Sub
End Class
Первый выделенный
фрагмент объявляет событие, а второй инициирует его при попытке недопустимого
повышения зарплаты.
Примерный
результат запуска программы показан на рис. 6.4. При нажатии кнопки ОК окно
сообщения исчезает, и в консольном окне выводится строка, из которой видно,
что зарплата Тома не изменилась.
Переменные WithEvents потребляют системные ресурсы. Как только такая перемен-ная становится ненужной, присвойте ей Nothing.
Рис.
6.4. Окно сообщения, вызываемое при обработке события
В предыдущем
примере мы воспользовались готовым классом System.EventArgs. Возможности этого
класса весьма ограничены, поскольку его конструктор вызывается без аргументов.
При более профессиональном подходе в программе определяется новый класс события,
дополняющий этот базовый класс. Например, в него можно включить ReadOnly-свойство,
возвращающее информацию о предполагаемом повышении зарплаты, и другое свойство
для текста сообщения. Пример подобного класса приведен ниже (решение CustomEventArgExample
в архиве). Запрашиваемый рост зарплаты и сообщение инкапсулируются в конструкторе
события. В дальнейшем для получения этих данных используются два свойства, доступных
только для чтения:
Public Class ImproperSalaryRaiseEvent
Inherits System.EventArgs
Private m_Message As String
Private m_theRaise As Decimal
Sub New(ByVal
theRaise As Decimal. ByVal theReason As String)
MyBase.New()
m_Message =
theReason
m_theRaise = theRaise
End Sub
Readonly Property
Message() As String
Get
Return m_Message
End Get End
Property Readonly Property theRaise() As Decimal
Get
Return m_theRaise
End Get
End Property
End Class
После того
как этот класс будет включен в решение, следует внести небольшие изменения в
объявление события в классе Empl oyee:
Public Event SalarySecurityEvent(ByVal Sender As
CustomEventArgExample.EmployeeWithEvents. ByVale As
ImproperSalaryRaiseEvent)
Теперь во
втором аргументе передается переменная класса ImproperSalaryRai seEvent. Следующие
изменения вносятся во фрагмент, в котором непосредственно вызывается событие:
Public Overloads Sub RaiseSalary(ByVal Percent As Decimal)
If Percent >
LIMIT Then
' Операция запрещена - необходим пароль
RaiseEvent SalarySecurityEvent(Me,
New ImproperSalaryRaiseEvent(Percent, "INCORRECT PASSWORD!"))
Else
m_Salary =(1 + Percent) * m_Salary
End If
End Sub
Остается
лишь слегка исправить код обработчика события (изменения выделены жирным шрифтом).
Module Modulel
Private WithEvents
anEmployee As EmployeeWithEventsII Sub Maine)
Dim tom As New EmployeeWithEventsII("Tom". 100000)
anEmployee =
tom
Console.Wntel_ine(tom.TheName &"has salary " & tom.Salary)
anEmployee.RaiseSalary(0.2D)'Суффикс D - признак типа Decimal
Console.WriteLine(tom.TheName & "still has salary " & tom.Salary)
Console.Writeline("Please press the Enter key")
Console.ReadLine()
End Sub
Public Sub anEmployee_SalarySecuhtyEvent(ByVal Sender _ As
CustomEventArgExample.EmployeeWithEvents. ByVal e As
CustomEventArgExample.ImproperSalaryRaiseEvent) Handles
anEmployee.SalarySecurityEvent
MsgBox(Sender.TheName & "had an improper salary raise of " & _ FormatPercent(e.theRaise) & "with INCORRECT PASSWORD!")
End Sub
End Module
Результат показан на следующем рисунке. Как видно из рисунка, данные о запрошенном росте заработной платы доступны в обработчике события.
Динамическая
обработка событий
Основной
проблемой синтаксиса WithEvents является его недостаточная гибкость. Обработчики
событий нельзя динамически устанавливать и отключать на программном уровне —
фактически вся схема обработки событий жестко фиксируется в программе. Однако
в VB .NET поддерживается другой способ динамической обработки событий, значительно
более гибкий. Он основан на возможности указания процедуры класса-приемника,
вызываемой при возникновении события (исключение добавленных обработчиков также
происходит динамически).
Конечно,
для установки обработчика события необходимо зарегистрировать не только класс-приемник,
но и метод, который должен вызываться при возникновении события. Для этой цели
применяется команда AddHandler, которой при вызове передаются два параметра:
Код AddHandl
ег включается в класс-приемник, а не в класс-источник. Адрес метода, вызываемого
при возникновении события, определяется оператором AddressOf. При вызове AddressOf
передается имя метода объекта класса-приемника. Например, следующая команда
устанавливает динамический обработчик события для объекта
tom:
AddHandler tom.SalarySecurityEvent.AddressOf
anEmp1oyee_SalarySecurityEvent
В результате
тестовая программа будет обнаруживать событие Sal arySecuri tyEvent объекта
tom и в случае его возникновения — вызывать процедуру anEmployee_SalarySecurityEvent
текущего модуля (разумеется, процедура anEmployee_SalarySecurityEvent должна
обладать правильной сигнатурой!).
Ниже приведен
фрагмент решения AddHandlerExamplel (ключевые строки выделены жирным шрифтом):
Module Modulel
Private WithEvents
anEmployee As EmployeeWithEvents Sub Main()
Dim torn As New EmployeeWithEvents("Tom". 100000)
Console.WriteLine(tom.TheName & "has salary " & tom.Salary)
AddHandler tom.SalarySecurityEvent,
AddressOf anEmployee_SalarySecurityEvent
tom.RaiseSalary(0.2D) ' Суффикс D - признак типа Decimal
Console.WriteLine(tom.TheName & "still has salary " & tom.Salary)
Console.WriteLine("Please press the Enter key")
Console. ReadLine()
End Sub
Public Sub anEmployee_SalarySecurity£vent(ByVal Sender _
As AddHandlerExamplel.EmployeeWi thEvents,_
ByVal e As AddHandlerExamplel.ImproperSalaryRaiseEvent)_
Handles anEmployee.SalarySecurityEvent
MsgBox(Sender.TheName
& "had an improper salary raise of " & _
FormatPercent(e.theRaise) & "with INCORRECT PASSWORD!")
End Sub
End Module
Команда AddHandler
обладает просто невероятной гибкостью. Например, установка обработчиков событий
может зависеть от имени типа:
If TypeName(tom)="Manager"
Then
AddHandler tom.SalarySecurityEvent.AddressOf _
anEmployee_SalarySecurityEvent
e
End If
Кроме того,
один обработчик событий можно связать с несколькими разными событиями, происходящими
в разных классах. Это позволяет выполнять в VB .NET централизованную обработку
событий с динамическим назначением обработчиков — в VB такая возможность встречается
впервые. В приведенном ниже листинге инициируются разные события в зависимости
от переданных параметров командной строки. Главное место в нем занимают фрагменты
вида
Case "first"
AddHandler m_EventGenerator.TestEvent,_
AddressOf m_EventGenerator_TestEventl
При передаче
в командной строке аргумента first устанавливается соответствующий обработчик
события.
В программе
используется полезный метод GetCommandLineArgs класса System.Environment. Как
упоминалось в главе 3, этот метод возвращает массив аргументов командной строки.
Начальный элемент массива содержит имя исполняемого файла; поскольку индексация
массива начинается с 0, для получения первого аргумента используется вызов System.Environment.GetComman3LineArgs(l),
однако предварительно
необходимо убедиться в существовании аргументов командной строки, для чего проверяется
длина массива System.Environment.GetCommandLineArgs. Перед запуском программы
перейдите на страницу Configuration Properties диалогового окна Project Properties
и укажите аргументы командной строки для тестирования.
Ниже приведен
полный исходный текст программы:
Option Strict
On Module Modulel
Private m_EventGenerator As EventGenerator
Sub Main()
m_EventGenerator=
New EventGenerator()
Dim commandLinesOAs
String = System.Environment.GetCommandLineArgs
If commandLines.Length
= 1 Then
MsgBox("No command argument.program ending!")
Environment.Exit(-l)
Else
Dim theCommand As String = commandLines(l)
Console.WriteLine("Thecommand lineoption is" StheCommand)
' Проверить параметр командной строки и назначить
' соответствующий обработчик события.
Select Case theCommand
Case "first"
AddHandler m_EventGenerator.TestEvent. AddressOf
m_EventGenerator_TestEvent1
Case "second"
AddHandler m_EventGenerator.TestEvent,_ AddressOf
m_EventGenerator_TestEvent2
Case Else
AddHandler m_EventGenerator.TestEvent. AddressOf
m_EventGenerator_TestEventDefault
End Select
' Инициировать события
m_EventGenerator.TriggerEvents()
End If
Console.WriteLine("Press enter to end.")
Console. ReadLine()
End Sub
'Обработчик по умолчанию для непустой командной строки
Public Sub m_EventGenerator_TestEventDefault(_
ByVal sender
As Object.ByVal evt As EventArgs) System.Console.WriteLine("Default choice
" & _
m_EventGenerator.GetDescri
pti on()) End Sub
' Обработчик 12 для строки "first"
Public Sub m_EventGenerator_TestEvent1(_
ByVal sender As Object.ByVal evt As EventArgs)
System.Console.WriteLineC'lst
choice " & _
m_EventGenerator.GetDescription())
End Sub
'Обработчик 13 для строки "second"
Public Sub m_EventGenerator_TestEvent2(
ByVal sender As Object.ByVal evt As EventArgs)
System.Console.WriteLinet"2nd choice " & _
m_EventGenerator.GetDescri
pti on ())
End Sub
End Module
Public Class
EventGenerator
' В классе определяется
только одно событие
Public Event
TestEvent(ByVal sender As Object, ByValevt As EventArgs)
' Также можно
было использовать конструктор по умолчанию
Public Sub New()
' Пустой конструктор
End Sub
.Public Function
GetDescription() As String
Return "EventGenerator class"
End Function
' Процедура вызывается для инициирования событий
Public Sub TriggerEvents()
Dim e As System.EventArgs = New System.EventArgs()
RaiseEvent TestEvent(Me.e)
End Sub
End Class
Отключение
обработчиков событий
Обработчики
событий, динамически назначаемые командой AddHandler, отключаются командой RemoveHandler,
которой должны передаваться точно такие же аргументы, как и при соответствующем
вызове AddHandlеr. Обычно для удаления динамически назначаемых обработчиков
хорошо подходит метод Dispose. По этой причине в каждом классе, использующем
динамическое назначение обработчиков, рекомендуется реализовать интерфейс IDisposable
— это напомнит пользователям класса о необходимости вызова Dispose.
Обработка
событий в иерархии наследования
Производный
класс может в любой момент инициировать открытые или защищенные события своего
базового класса, при этом событие идентифицируется ключевым словом MyBase.
Кроме того, производные классы автоматически наследуют все обработчики открытых
и защищенных событий своих предков. Время от времени в производном классе возникает
необходимость в переопределении методов, используемых при обработке открытых
и защищенных событий базового класса. Для этой цели используется конструкция
Handles MyBase. Пример:
Public Class
ParentClass
Public Event ParentEventtByVal aThing As Object.
ByVal E As System.EventArgs)
' Программный
код End Class
' Производный класс
Public Class
ChildClass
Inherits ParentClass
Sub EventHandler(ByVal x As Integer)
Handles MyBase ParentEvent
'Обработка событий
базового класса
End Sub
End Class
При использовании
механизма обратного вызова приходится выполнять вспомогательные операции для
регистрации вызываемых функций. В оставшейся части главы будет показано, что
при этом происходит и как при помощи этих операций добиться максимальной эффективности
обратного вызова.
Механизм
обратного вызова (а следовательно, и события) в VB .NET зависит от особой разновидности
объектов .NET, называемых делегатами. Делегат является экземпляром класса
System.Delegate. В простейшем случае в делегате инкапсулируется объект и адрес
заданной функции или процедуры этого объекта. Такие делегаты идеально подходят
для схем обратного вызова вроде той, что используется при обработке событий.
Почему? Потому что делегат содержит всю информацию, необходимую для обратного
вызова, и может использоваться для вызова нужного метода объекта-приемника.
Но прежде,
чем переходить к описанию работы с делегатами, стоит подчеркнуть одно важное
обстоятельство. Хотя обработка событий на платформе .NET основана на использовании
делегатов, в подавляющем большинстве случаев вам не придется работать непосредственно
с делегатами. Команда AddHandl ег предоставляет в ваше распоряжение все
необходимое для гибкой обработки событий в VB .NET (впрочем, как вы вскоре увидите,
у делегатов есть и другие применения).
В
делегатах также могут инкапсулироваться общие методы класса без привязки ккон-кретному
экземпляру. Кроме того, в групповых делегатах инкапсулируется сразу несколько
объектов с несколькими процедурами.
Традиционные
средства вызова функции по адресу были основаны на языковой поддержке указателей
на функции. Как показано в следующей врезке, применение указателей на функции
сопряжено с потенциальным риском. По сравнению с указателями на функции делегаты
обеспечивают дополнительную защиту, значение которой не стоит недооценивать.
Указатели на функции в VB6.При вызовах функций API часто передается адрес функции для обратного вызова, поэтому в VB6 поддерживался оператор AddressOf. В VB6 адрес функции мог передаваться при любом вызове API. Но что происходило, если список параметров функции, адрес которой передавался при вызове, отличался от предполагаемого? Обычно это приводило к общей ошибке защиты (GPF) и даже к фатальным сбоям с появлением синего экрана.
Таким
образом, произвольные указатели на функции обладают принципиальным недостатком:
компилятор не может проверить, что такой указатель относится к функции правильного
типа. Делегаты представляют собой разновидность указателей на функции, безопасных
по отношению к типам. Следуя принципу «доверяй, но проверяй», компилятор
автоматически проверяет сигнатуру вызываемой функции — такой вариант работает
гораздо надежнее.
Начнем с
создания простейшего делегата, инкапсулирующего объект и «указатель»
на процедуру этого объекта. Как показано ниже, синтаксис создания объектов чуть
сложнее синтаксиса, используемого при создании простых объектов. Прежде всего
нам понадобится класс, содержащий процедуру с определенной сигнатурой:
Class ClassForStringSubDelegate
' Использовать
конструктор по умолчанию
Public Sub TestSub(ByVal aString As String)
Console. WriteLine(aString
SaString)
End Sub
End Class
Чтобы создать
делегат для обратного вызова этой процедуры, необходимо сообщить компилятору
об использовании делегата для процедуры с одним строковым параметром. Первый
шаг этого сценария выполняется за пределами Sub Main следующей строкой:
Public Delegate Sub StringSubDelegate(ByVal
aString As String)
Обратите
внимание: в этой строке мы не объявляем делегат, а определяем его. Компилятор
VB .NET автоматически создает новый класс StringSubDel egate, производный от
System . Delegate1.
Далее в процедуре
Sub Main экземпляр класса делегата создается оператором AddressOf для адреса
процедуры, имеющей правильную сигнатуру. VB .NET автоматически вычисляет объект
по полному имени процедуры. Команда создания экземпляра выглядит так:
aDel egate =
AddressOf test.TestSub
Компилятор
VB .NET понимает, что делегат создается для объекта test. Также можно воспользоваться
ключевым словом New, однако это делается редко, поскольку New неявно вызывается
в первой форме:
aDelegate =
New StringSubDelegate(AddressOf test.TestSub)
После того
как делегат будет создан, инкапсулированная в нем процедура вызывается методом
Invoke класса Delegate, как в следующем фрагменте:
Sub Main( )
Dim test As
New ClassForStri ngSubDelegate()
Dim aDelegate
As StringSubDelegate
aDelegate =
AddressOf test.TestSub
aDelegate.Invoke(
"Hello" )
Console. ReadLineb
End Sub
На
самом деле использовать Invoke необязательно — достаточно передать делегату
нужные параметры. VB .NET поймет команду aDelegate(" Hello"), которая
выглядит значительно проще.
В этом нетрудно
убедиться, просматривая полученный IL-код при помощи программы ILDASM.
Согласитесь,
такой способ вывода в консольном окне строки «HelloHello» выглядит
несколько необычно!
Впрочем,
«если это и безумие, то в своем роде последовательное». Предположим,
вы решили усовершенствовать свой класс, чтобы вместо простого вывода текста
в консольном окне на экране появлялось окно сообщения. Для этого достаточно
внести изменения, выделенные жирным шрифтом в следующем листинге:
Module Modulel
Public Delegate Sub StringSubDelegate(ByVal aString As String)
Sub Main()
Dim test As
New ClassForStringSubDelegate()
Dim aDelegate
As StringSubDelegate
aDelegate -
AddressOf test.TestMsgBox
aDelegate("Hello")
Console. ReadLine()
End Sub
Class ClassForStringSubDelegate
' Использовать конструктор по умолчанию
Public Sub TestSub(ByVal
aString As String)
Console.WriteLine(aString SaString)
End Sub
Public Sub TestMsgBox(ByVal
aString As String)
MsgBox(aString &aString)
End Sub
End Class End
Module
Поскольку
для делегата важна только сигнатура инкапсулированного метода, он легко «переключается»
на другой метод. Потребовалось создать новую версию для вывода информации в
окне отладки (вместо консоли и окна сообщения)? Достаточно внести несколько
изменений в делегат и добавить в класс функцию, инкапсулируемую делегатом.
Важнейшая
особенность делегатов заключается в том, что связывание с методом производится
на стадии выполнения. Таким образом, делегаты в сочетании с явным или
неявным вызовом метода Invoke по своим возможностям значительно превосходят
функцию VB6 CallByName.
Практический
пример: специализированная сортировка
Предыдущие примеры выглядят искусственно и относятся к категории «игрушечных программ». В этом разделе мы покажем, как использовать делегаты при специализированной сортировке — одной из стандартных областей применения функций обратного вызова. Общая идея заключается в том, что один метод сортировки в зависимости от ситуации может использовать разные критерии сортировки. Предположим, у вас имеется массив имен: «Mike Item», «Dave Mendlen», «Alan Carter», «Tony Goodhew», «Ari Bixhorn», «Susan Warren»-.
Если вызвать
метод Sort класса Array, сортировка будет произведена по именам. А если вы хотите
отсортировать массив по фамилиям?
Возможный
подход к решению этой задачи описан в главе 5 — вы можете написать собственную
реализацию интерфейса IComparer и передать ее Sort. Применение обратного вызова
на базе делегатов выглядит чуть более элегантно и теоретически обладает большей
гибкостью, поскольку программист может определить собственные процедуры сортировки,
которые в конкретной ситуации работают эффективнее стандартных алгоритмов.
Чтобы массив
поддерживал сортировку по именам, следует определить класс с несколькими методами
Compare и при помощи делегата связать алгоритм сортировки с нужным методом Compare
через механизм обратного вызова. В частности, это позволит динамически изменять
критерий сортировки во время работы программы.
Прежде всего
определяется класс, выполняющий сортировку. Чтобы избежать подробного обсуждения
различных алгоритмов сортировки, мы воспользуемся простейшим алгоритмом волновой
сортировки:
For i =bottom
To (top - bottom) For j =i + 1 To top
If Stuff(j) < Stuff(i))Then
temp = Stuff(i)
Stuff(i) = Stuff(j)
Stuff(j) = temp
End If
Next j
Next I
Чтобы реализовать
этот алгоритм с применением функций обратного вызова, необходимо определить
класс Special Sort с делегатом, используемым при обратном вызове. Код этого
класса приведен ниже:
1 Public Class
Special Sort
2 ' Определение
делегата
3 Public Delegate Function SpecialCompareCallback(ByVal flrstString _
As String,ByVal
secondString As String) As Boolean
4 ' Определение
процедуры, вызываемой делегатом
5 Public Shared Sub IfySort(ByVal Stuff As String()._
ByVal MyCompare
As SpecialCompareCallback)
6 Dim i, j As
Integer
7 Dim temp As
String
8 Dim bottom
As Integer = Stuff.GetLowerBound(0)
9 Dim top As
Integer = Stuff.GetUpperBound(0)
10 For i = bottom
To (top = bottom)
11 For j = i
+ 1 To top
12 If MyCompare(Stuff(j).
Stuff(i)) Then
13 temp = Stuff(i)
14 Stuff(1)
- Stuff (j)
15 Stuff(j)
= temp
16 End If
17 Next j
18 Next i
19 End Sub
20 End Class
В строке
З определяется делегат, при помощи которого классу передается информация об
используемом порядке сортировки. Делегат может инкапсулировать любую функцию,
которая, как и все нормальные функции сравнения строк, получает два строковых
параметра и возвращает логическую величину.
В строке
5 определяется общая процедура, одним из параметров которой является переменная
с типом делегата. Таким образом, в ключевой строке 12:
If MyCompare(Stuff(j).
Stuff(i)) Then
функция сравнения,
инкапсулированная в делегате MyCompare, может относиться к другому классу! Например,
если определить приведенный ниже класс, эта схема позволит использовать любой
из его методов Compare (обратите внимание: методы Compare объявлены общими,
поэтому для их вызова нам даже не нужно создавать конкретный экземпляр класса):
Public Class
MyCustomCompare
Public Shared
Function TheBasicComparetByVal firstString As String,
ByVal secondString As String) As Boolean
Return (firstString <- secondString)
End Function
Public Shared
Function TheSpecialCompare(ByVal firstString As String.
ByVal secondString As String)As Boolean Dint tokensl,tokens2 As String()
tokensl = firstString.Split(Chr(32))
tokens2 = secondString.Split(Chr(32))
Return (tokensl(l) <- tokens2(l))
' Сравнение по фамилии!
End Function
End Class
Класс содержит
две общие функции, которые ниже будут использованы для создания делегатов. Первая
функция, TheBasicCompare, просто сравнивает строки в алфавитном порядке. Более
интересная функция TheSpecialCompare предполагает, что строка передается в формате
«имя фамилия», и сравнивает фамилии, выделяя их при помощи удобной
функции Split.
Остается
лишь создать экземпляры класса SpecialSort и делегаты. Это происходит в следующей
функции Main (ключевые строки выделены жирным шрифтом):
1 Module Modulel
2 Sub Main()
3 Dim test()As
String ={"Mike Iem"."Dave Mendlen"."Alan Carter".
4 "Tony
Goodhew","An Bixhorn"."Susan Warren"}
5 ' Объявить
переменную обратного вызова в форме класс.делегат
6 Dim MyCallBack
As Special Sort.SpecialCompareCal1back
7 MyCallBack
= AddressOf MyCustomCompare.TheBasicCompare
8 SpecialSort.MySort(test,MyCallBack)
9 Console.WriteLine("Here
is a basic sort by FIRST name")
10 Dim temp
As String
11 For Each
temp In test
12 Console.WriteLine(temp)
13 Next
14 ' Передать
другую процедуру сравнения
15 MyCallBack
= AddressOf MyCustomCompare.TheSpecialCompare
16 Sped al Sort.
MySort (test. MyCallBack)
17 Console.WriteLine()
18 Console.WriteLineC'Here
is a sort by LAST name")
19 For Each
temp In test
20 Console.WriteLine(temp)
21 Next
22 Console.
ReadLine()
23 End Sub
24 End Module
В строке
6 объявляется «псевдоуказатель на функцию». Чтобы задать его значение,
мы передаем адрес функции с правильной сигнатурой (строки 7-15). Поскольку функции
объявлены общими, создавать экземпляр класса MyCustomCompare для этого не нужно.
После создания делегата в строках 8 и 16 вызывается нужная процедура сортировки
класса Special Sort. Поскольку при вызове MySort передается делегат, процедура
обращается к классу MyCustomCompare и узнает, по какому критерию должно осуществляться
сравнение.
В приведенных
выше примерах в делегате инкапсулировался адрес одной функции или процедуры.
Нередко в делегатах требуется инкапсулировать сразу несколько процедур (инкапсуляция
нескольких функций особого смысла не имеет — каким должно быть возвращаемое
значение?). Подобные делегаты называются групповыми (multicast) и реализуются
в виде делегата, содержащего несколько однотипных делегатов. При наличии группового
делегата все инкапсулированные процедуры вызываются одним методом Invoke, причем
это происходит в соответствии с порядком занесения их делегатов в групповой
делегат.
Чтобы создать
групповой делегат, следует объединить минимум двух делегатов одного типа и присвоить
результат переменной того же типа. Задача решается статическим методом Combine
класса System.Delegate, который возвращает новый делегат.
Допустим, firstDel и secDel — экземпляры класса MyMultiCastDelegate. Следующая команда объединяет firstDel и secDel в групповой делегат, хранящийся в
firstDel: firstDel
=System.Delegate.Combine(firstDel,secDel)
Ниже приведено
простое приложение, объединяющее адреса нескольких функций в групповом делегате:
1 Option Strict
On
2 Module Modulel
3 Sub Main()
4 Console.WriteLine("Calling
delegate function...")
5 RegisterDelegate(AddressOf
CallBackHandlerl)
6 RegisterDelegate(AddressOf
CallBackHandler2)
7 Call Delegates
()
8 Console.WriteLine(
9 "Finished
calling.delegate function...")
10 Console.ReadLine()
11 End Sub
12 Public Sub
CallBackHandlerHByVal lngVal As RETURNJALUES)
13 Console.WriteLine("Callback
1 returned " & IngVal)
14 End Sub
15 Public Sub
CallBackHandler2(ByVallngVal As RETURNJALUES)
16 Console.WriteLine("Callback
2 returned " & IngVal)
17 End Sub
18 End Module
19 Module Module2
20 Public Delegate
Sub CallBackFunc(ByVallngValAs RETURN_VALUES)
21 Private m_cbFunc
As CallBackFunc
22 Public Enum
RETURN_VALUES
23 VALUE_SUCCESS
24 VALUE_FAILURE
25 End Enum
26 Public Sub
RegisterDelegate(ByRef cbFunc As CallBackFunc)
27 m_cbFunc
= CType(System.Delegate.Combine(_
28 m_cbFunc.cbFunc).CallBackFunc)
29 End Sub
30 Public Sub
Call Delegates ()
31 Dim IngCounter
As Long = 0
32 ' Вызвать
процедуры через делегата
33 ' и вернуть
признак успешного вызова
34 m_cbFunc(RETURN
VALUES.VALUE_SUCCESS)
35 End Sub
36 End Module
В строках 5 и 6 вызывается процедура модуля Module2 (строки 26-28), где и происходит фактическое построение группового делегата. Это возможно благодаря тому, что делегат передается по ссылке, а не по значению. Обратите внимание на преобразование типа метода Combine к типу делегата в строке 27. Непосредственный вызов функций группового делегата происходит в строках 30-35. Всем зарегистрированным функциям передается значение перечисляемого типа RETURNJALUES . VALUE_SUCCESS. Результат выполнения программы показан на рисунке.
Групповые
делегаты как члены классов
В предыдущем
примере все модули имеют доступ ко всем функциям остальных модулей. Такую архитектуру
нельзя признать удачной — правильнее было бы оформить делегат в виде члена класса,
нежели в виде открытого объекта. Это позволит выполнить перед его созданием
проверку, аналогичную той, которая выполняется для других членов класса. Ниже
приведен слегка измененный вариант предыду-
щей архитектуры,
где перед дополнением группового делегата новыми функциями выполняется проверка
(в данном примере — весьма тривиальная). Соответствующий фрагмент выделен жирным
шрифтом:
Option Strict
On
Public Class
DelegateServer
Public Delegate Sub ClientCallback(ByVal IngVal As Long)
Private m_Clients As ClientCallback
' Использовать конструктор по умолчанию
Public Sub RegisterDelegate(ByVal
aDelegate As
ClientCallback.ByVal dolt As Boolean)
' Обычно здесь выполняется полноценная проверка.
' В данном примере функция обратного вызова регистрируется
' лишь в том случае, если второй параметр равен
True. If dolt
Then
m_Clients =
CType(System.Delegate.Combine(m_ Clients.aDelegate)._
ClientCallback)
End If
End Sub
Public Sub CallClients(ByVal
IngVal As Long)
m_Clients( IngVal)
End Sub
End Class
Module Modulel
Sub Main()
Dim delsrv As
New DelegateServer()
delsrv.RegisterDelegate(AddressOf
DelegateCallbackHandlerl.True)
' He вызывается
- второй параметр равен False!
delsrv.RegisterDelegate(AddressOf
DelegateCal1backHandler2.False)
' Инициировать
обращение к клиентам
delsrv.CallClients(125)
Console.WriteLine("Press
enter to end.")
Console.ReadLine()
End Sub
Public Sub DelegateCallbackHandlerKByValIngVal
As Long)
System.Console.WriteLine("DelegateCa11backHandlerl cal1ed")
End Sub
Public Sub DelegateCallbackHandler2(ByVal
IngVal As Long)
System.Console.Wri teLine("DelegateCal1backHandler2 cal1ed")
End Sub
End Module
Мы рассмотрели
разнообразные примеры использования делегатов, однако ни один из них не имел
отношения к обработке событий. Впрочем, связь между делегатами и событиями в
VB .NET весьма проста. При каждом использовании сокращенного синтаксиса обработки
событий, описанного в первой половине главы, VB .NET незаметно определяет класс
делегата для обработки события, а команда AddressOf создает экземпляр делегата
для этого обработчика. Например, следующие две строки эквивалентны (EventHandler
— имя неявно определяемого делегата):
AddHandler Buttonl.Click.AddressOf
Me.Buttonl_Click
AddHandler Buttonl.Click.New
EventHandler(AddressOf Buttonl Click)
В сущности,
каждое событие соответствует делегату следующего вида:
Public Delegate
Event (sender As Object.evt As EventArgs)
Вызов RaiseEvent просто приводит к вызову Invoke для автоматически сгенерированного делегата.
Дело в том, что в его постановке и выводах произведена подмена, аналогичная подмене в школьной шуточной задачке на сообразительность, в которой спрашивается:
- Cколько яблок на березе, если на одной ветке их 5, на другой ветке - 10 и так далее
При этом внимание учеников намеренно отвлекается от того основополагающего факта, что на березе яблоки не растут, в принципе.
В эксперименте Майкельсона ставится вопрос о движении эфира относительно покоящегося в лабораторной системе интерферометра. Однако, если мы ищем эфир, как базовую материю, из которой состоит всё вещество интерферометра, лаборатории, да и Земли в целом, то, естественно, эфир тоже будет неподвижен, так как земное вещество есть всего навсего определенным образом структурированный эфир, и никак не может двигаться относительно самого себя.
Удивительно, что этот цирковой трюк овладел на 120 лет умами физиков на полном серьезе, хотя его прототипы есть в сказках-небылицах всех народов всех времен, включая барона Мюнхаузена, вытащившего себя за волосы из болота, и призванных показать детям возможные жульничества и тем защитить их во взрослой жизни. Подробнее читайте в FAQ по эфирной физике.