Идеология C#: события и делегаты |
![]() |
Добавил(а) Даниил Захаров |
Роль информационных технологий в современном мире сложно недооценить. Они начали развиваться задолго до появления первых компьютеров, но в то время выглядели как набор математических моделей и щелкающих устройств [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. Конструктор принимает один аргумент – функцию с подходящей сигнатурой. Какой – см. в начале статьи. При этом если событие клика по кнопке будут слушать еще и другие объекты, то все обработчики будут вызваны в том порядке, в котором они подключались к событию. Отсюда возникают разного рода проблемы, и методы их решения давно известны и описаны. При необходимости можно найти всю информацию в интернете. Напоследок осталось отметить механизм запуска делегата на выполнение. Вот самая простая схема: - событие описывается как экземпляр делегата; Стоит только оговорить некоторые правила именования методов и событий: события именуются глаголами в форме Past Simple (не Connect, но Connected), а имя инициализатора получается из имени события добавлением On (например, для события Connected инициализатор должен иметь имя OnConnected). При этом не рекомендуется называть события так, чтобы они начинались с этого самого «On» (согласитесь, неприятно читать метод с именем «OnOnConneted»). Автор статьи Даниил Захаров (harikomp@list.ru), Кафедра ЭИИС ("Эргономика и информационно-измерительные системы", eiis@mail.ru, +7 (495) 915-07-28), МАТИ-РГТУ им. К. Э. Циолковского, г. Москва. [Ссылки] 1. Википедия: История вычислительной техники. |