Роль информационных технологий в современном мире сложно недооценить. Они начали развиваться задолго до появления первых компьютеров, но в то время выглядели как набор математических моделей и щелкающих устройств [1]. С начала 40-х годов началось их бурное развитие, и тогда встал вопрос о четком разграничении типов данных (сначала их делили на строки и числа; вскоре каждый из типов породил множество потомков). Для удобства программирования их стали объединять в то, что в стандартах по C-ориентированным языкам называется структурой.
Однако вскоре и этого стало мало. Подпрограммы все чаще стали ссылаться на некоторые сгруппированные данные, и тогда встал вопрос об организации классов. В то время классы рассматривались как структуры, которые помимо данных содержат в себе исполняемый код. Примерно тогда же (а это было еще при мейнфреймах) встал вопрос, примерно звучащий следующим образом: «А нельзя ли закрепить за некой группой функций одну сигнатуру и после этого вызывать любую из них по динамической ссылке (читай – «вызывать как переменную»)?».
Ответ на этот вопрос был утвердительным, и найденное решение позволило ввести понятие функционального типа данных. Если на пальцах: в переменную типа int мы можем записывать целые числа:
int a = 5;
В переменные типа string мы можем записывать строки:
string MyString = "Тестовая строка";
Однако если объявить какую-то функцию, например (ничего не напоминает?):
private void Button1_Click(object sender, EventArgs e)
и при этом задать некую сигнатуру следующим образом:
public delegate void EventHandler(object sender, EventArgs e)
то мы можем записывать в переменные типа EventHandler (мы только что описали этот тип: он может хранить ссылки на функции типа void, принимающие аргументы типа object и EventArgs) значения, например описанную выше функцию Button1_Click. Действительно, сигнатуры совпадают: описанная функция Button1_Click тоже возвращает void и принимает sender и EventArgs.
Но не все так просто. Мы описали то, что в документации по С-ориентированным языкам называется делегатом. Как уже отмечалось, это нечто, напоминающее переменную, способную выполнять роль указателя на функцию. В С++ она может быть описана, например, таким образом:
void (*EventHandler) (void* sender, EventArgs* e)
(прошу меня поправить, если напутал с синтаксисом, ибо на С++ не писал вообще и с указателями не дружу. Это должен быть указатель на функцию, принимающую указатель на void и указатель на EventArgs)
Однако последнее определение задаст экземпляр – что-то, хранящее всего одно значение, одну ссылку. Описание делегата в этом смысле напоминает описание класса – оно описывает семейство экземпляров. Для того, чтобы воспользоваться функционалом, необходимо (как и для любого другого объекта) сначала объявить экземпляр, а уже затем им пользоваться. В следующем примере описывается делегат, а затем его экземпляр объявляется в пользовательском классе и к нему подключается слушатель (по сути мы записываем в переменную ссылку на функцию):
// Описываем делегат
public delegate double MyDelegate(double v);
// Демонстрационный класс
class Testings
{
// Создаем его экземпляр
public MyDelegate D;
public Testings()
{
// В конструкторе подключим к нему слушателя:
// собственный метод экземпляра демонстрационного класса
D = this.Square;
}
// Это может быть любой метод.
// Необходимо лишь задать верную сигнатуру делегата.
public double Square(double val)
{
return val * val;
}
}
class Program
{
static void Main(string[] args)
{
Testings t = new Testings();
Console.WriteLine("Вызов функции с параметром 2: {0}", t.Square(2).ToString());
Console.WriteLine("Вызов Делегата с параметром 2: {0}", t.D(2).ToString());
Console.ReadKey();
}
}
Результат виден на скриншоте ниже:
Итак, в пространстве имен мы объявили делегат, принимающий double и возвращающий double. В своем классе мы определили метод Square с той же сигнатурой, который возвращает квадрат аргумента, и экземпляр делегата D. В конструкторе мы подключаем к экземпляру делегата D метод Square. Теперь мы можем вызвать D снаружи точно так же, как мы бы вызывали Square, что, собственно, и делаем в консольном приложении. Результат одинаков (что неудивительно для тех, кто понял принцип работы).
Однако это еще не все. Экземпляр делегата может хранить не просто ссылку на функцию, а (в отличие от примера с указателем в С++) цепочку вызовов. Это удобно для организации событий. В классе, объявившем внутри себя экземпляр делегата с ключевым словом event, можно организовать такой принцип работы, что в некоторые необходимые моменты экземпляр делегата (далее будем говорить «событие») запускается на выполнение. При этом могут вызываться методы, не принадлежащие классу-хозяину события.
Это ключевое положение, обеспечивающее работу механизма событий: оба класса (и реализующий событие, и пользующийся им) заключают соглашение, где оговаривается сигнатура функции-обработчика события (описывается делегат). После этого класс, реализующий события, может «не беспокоиться» о том, что он передаст в функцию через событие не те аргументы, которые ждет класс-обработчик события.
Стоит обязательно отметить, что классов-обработчиков события может быть несколько, и тогда каждый из них подключает свою процедуру-обработчик к соответствующему событию примерно следующим образом (пускай мы подключаем к событию Click объекта Button1 слушателя из описанного в начале статьи примера):
this.button1.Click += new System.EventHandler(this.button1_Click);
Эта строка скопирована из модуля Form1.Designer.cs обычного C#-проекта, где на форму накинута кнопка, а событие клика по ней обрабатывается внутри класса Form1. Более того: эта строка генерируется автоматически, и многие пользователи даже не знают о том, что она есть. При этом их проекты работают, да и живут припеваючи. Но нам это жизненно необходимо для понимания основного материала статьи.
Разберем последний листинг. На форме (к ней мы обращаемся квалифицированно, т.е. через this) есть кнопка с именем button1. В классе Button описано событие Click следующим образом:
public event EventHandler Click;
EventHandler – стандартный класс платформы .Net, позволяющий пользоваться механизмами событий в подавляющем большинстве случаев. Как можно заметить, его описание расположено в пространстве имен System. Конструктор принимает один аргумент – функцию с подходящей сигнатурой. Какой – см. в начале статьи.
При этом если событие клика по кнопке будут слушать еще и другие объекты, то все обработчики будут вызваны в том порядке, в котором они подключались к событию. Отсюда возникают разного рода проблемы, и методы их решения давно известны и описаны. При необходимости можно найти всю информацию в интернете.
Напоследок осталось отметить механизм запуска делегата на выполнение. Вот самая простая схема:
- событие описывается как экземпляр делегата; - в произвольный момент времени к нему могут быть подключены слушатели, а могут быть и не подключены (тогда значение в экземпляре null); - описывается так называемый инициатор события: метод произвольной сигнатуры (обычно private), который запускает делегат на выполнение; - определяется место в коде класса, в котором событие должно наступить. Туда вставляется вызов инициатора события.
Стоит только оговорить некоторые правила именования методов и событий: события именуются глаголами в форме Past Simple (не Connect, но Connected), а имя инициализатора получается из имени события добавлением On (например, для события Connected инициализатор должен иметь имя OnConnected). При этом не рекомендуется называть события так, чтобы они начинались с этого самого «On» (согласитесь, неприятно читать метод с именем «OnOnConneted»).
Автор статьи Даниил Захаров (harikomp@list.ru), Кафедра ЭИИС ("Эргономика и информационно-измерительные системы", eiis@mail.ru, +7 (495) 915-07-28), МАТИ-РГТУ им. К. Э. Циолковского, г. Москва.
[Ссылки]
1. Википедия: История вычислительной техники. 2. Класс-обертка для AVR-USB-MEGA16 с поддержкой событий. 3. C#: зачем нужны InvokeRequired и Invoke? |