Программирование PC Идеология C#: события и делегаты Sat, December 07 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Идеология 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();
        }
    }

Результат виден на скриншоте ниже:

Csharp-delegate-output-example01

Итак, в пространстве имен мы объявили делегат, принимающий 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 с поддержкой событий.
3C#: зачем нужны InvokeRequired и Invoke?

 

Добавить комментарий


Защитный код
Обновить

Top of Page