Что такое делегат в программировании
Перейти к содержимому

Что такое делегат в программировании

  • автор:

Что такое делегат в программировании

На данном этапе подготовки к собеседованию кандидат уже должен понимать чем является переменная и как приписать ей значение. Язык C# явялется языком с сильной типизацией, то есть всегда известен тип переменной, а значит известно какие значения мы можем приписать к данной переменной. Работая с переменным может возникнуть ряд вопросов:

  1. Можно ли создать такую переменную, к которой в качестве значения будет приписана ссылка на метод?
  2. Можно ли к такой переменной приписать любой метод?
  3. Можно ли к такой переменной приписать несколько методов?
  4. Как такую переменную можно использовать и что будет являться результатом этого использования?
  5. Существуют ли в языке C# предопределенные переменные такого типа?

Ответим по порядку на каждый из этих вопросов.

Ответ на вопрос 1. Да, можно создать переменную, к которой можно приписать ссылку на метод. В языке C# такая переменная будет называться делегатом. Расширяя определение делегата, данное в предыдущем предложении, стоит сказать что при декларировании делегата необходимо указать сигнатуру метода, который можно будет приписать данному делегату. Сигнатурой метода называется определенный набор аргументов метода по типу, количеству и порядку. В одном из предыдуших видео, посвященному ключевым словам при наследовании, я ошибся, сказав, что в сигнатуру метода входит также и тип возвращаемого значения. Исправляю свою ошибку – в сигнатуру метода тип возвращаемого значения не входит.

Ответ на вопрос 2. Исходя из вышесказанного, к делегату нельзя приписать любой метод. К делегату может быть приписан метод, который имеет сигнатуру соответствующую сигнатуре делегата.

Ответ на вопрос 3. Да, к делегату можно приписать несколько методов которые имеют соответсвующую сигнатуру. В этой же ситуации некоторые или все методы можно «отписать» от делегата.

Ответ на вопрос 4. Делегат является переменной, которая содержит указатель на методы или методы. Соответственно используя делегат мы можем вызывать методы, указатели на которые содержит делегат. Результатом использования делегата будет выполнение метода и, если необходимо, возвращение результата выполнения метода.

Ответ на вопрос 5. Да, в языке C# существуют предопределенные делегаты. Перед их более детальным рассмотрением, подумаем зачем они были созданы. В обычной ситуации созданный делегат будет использоваться скорей всего в одном месте в коде, использование его повсеместно будет затруднено в силу его узкой специализации или специфического названия, которое может не подходить к некоторым контекстам. В связи с этим часто бывают ситуации, когда имя делегата по сути не важно, но важно чтобы был определенный делегат с определенной сигнатурой. С этой целью возникли предопределенные делегаты – существуют 2 большие группы таких делегатов Action и Func. Это обобщенные делегаты, которые могут принимать до 16-ти типовых параметров. Отличие этих групп состоит в следующем – делегаты Action соответствуют методам, которые не возвращают значения (то есть возвращают void), а делегаты Func соответствуют методам, которые возвращают значение. Последней типовой параметр в делегате Func – это тип возвращаемого значения.

Делегаты и события

Делегаты являются ссылками на методы, инкапсулирующими настоящие указатели и предоставляющими удобные сервисы для работы с ними. Ссылки представляют собой объекты соответствующего типа. Все делегаты являются объектами типа System.Delegate или System.MulticastDelegate, который является производным от первого.

Уже к окончанию разработки среды исполнения, ее архитекторы пришли к выводу, что целесообразно использовать только класс MulticastDelegate, а класс Delegate является избыточным. Но, опасаясь серьезных архитектурных нестыковок, разработчики были вынуждены оставить класс Delegate в составе общей библиотеки классов.

Различие между этими классами заключается в том, что экземпляры первого (делегаты) могут хранить лишь одну ссылку на метод, а экземпляры второго могут содержать сразу несколько ссылок на методы. Благодаря этому, можно подсоединять к одному делегату несколько методов, каждый из которых при единственном обращении к делегату будет вызваться по цепочке. Таким образом, из программы будет виден лишь один делегат, за которым скрывается несколько методов (рисунок 1).

Эта возможность очень удобна для поддержки событий, поскольку позволяет без использования дополнительных механизмов присоединить к событию несколько функций обработчиков. Фактически, делегат представляет собой объект — черный ящик, скрывающий в своих недрах указатели на функции. Важно понять, что делегаты, по сути дела, ничем не отличаются от обычных пользовательских объектов. Главная их особенность состоит лишь в том, что они имеют поддержку со стороны среды исполнения. Об их свойствах, в отличие от обычных объектов, знают даже компиляторы, предоставляющие удобные специальные сервисы для работы с ними.

Рисунок 1. Делегат может ссылаться на несколько методов или функций.

Виды методов

Все методы в среде .NET можно разделить на две группы: статические (static) и экземплярные (instance).

Если делегат ссылается на статический метод, то все действительно просто. Так как в этом случае есть вся необходимая для вызова метода информация: адрес метода и параметры. Если же делегат ссылается на экземплярный метод, то задача усложняется. Чтобы вызвать экземплярный метод, делегату необходимо знать ссылку на объект, к которому привязан данный конкретный метод. Оказывается, что эта ссылка хранится в самом объекте делегата и указывается при его создании. На протяжении всей жизни объекта делегата данная ссылка не изменяет своего значения, она всегда постоянна и может быть задана только при его создании. Таким образом, вне зависимости от того, ссылается ли делегат на статическую функцию или на экземплярный метод, обращение к нему извне ничем отличаться не будет. Всю необходимую функциональность обеспечивает сам делегат, вкупе со средой исполнения. Это очень удобно, поскольку множество разных делегатов можно привязывать к одному событию.

Делегаты – начинаем непосредственную работу

Создаем собственный делегат

Итак, делегат представляет собой экземпляр пользовательского класса, являющегося потомком класса MulticastDelegate. Соответственно, необходимо уметь объявлять подобные классы. Напрямую проделать операцию наследования от класса MultiCastDelegate или Delegate не получится, при попытке сделать это, компилятор выдаст следующую ошибку.

В переводе на русский язык сообщение означает: «Hello не может быть наследником специального класса System.Delegate». Наследование запрещает сам компилятор и только для этих классов, потому что в большинстве компиляторов предусмотрены специальные средства для работы с делегатами. В C# это специальная конструкция, начинающаяся с ключевого слова delegate .

При описании делегата мы должны указать прототип метода, на который будут ссылаться экземпляры данного делегата. В общем виде описание делегата будет выглядеть следующим образом.

Описывая делегат, необходимо понимать, что вводится новый тип, а именно, класс-потомок MulticastDelegate. То есть конструкция, приведенная выше, на самом деле гипотетически порождает примерно такой код:

Компилятор проводит такую трансляцию неявно, дабы не пугать рядовых программистов ужасами внутренней архитектуры делегатов. Таким образом, для того чтобы воспользоваться делегатом, необходимо создать экземпляр данного класса, который бы указывал на нужный нам метод. Сам по себе делегат является типом и никаких ссылок на метод содержать не может, а соответственно, не может и использоваться для обращения к ним. Для этого необходимо использовать экземпляр делегата.

Создание экземпляра делегата просто, поскольку компилятор уже подготовил все необходимое. Он создал специальный конструктор, которому нужно передать указатель на метод, а со всем остальным разберется среда исполнения.

Создание экземпляра делегата происходит следующим образом:

Где MyDelegate — это тип делегата, del — экземпляр делегата, который будет создан в результате выполнения конструкции, MyHandler — метод, на который будет ссылаться этот делегат. Соответственно, после создания экземпляра делегата можно обращаться к методам, на которые он ссылается. В языках высокого уровня существует возможность обращаться к экземпляру делегата, как к самому методу. Выглядеть это будет так.

Эта строка вызовет метод MyHandler, на который ссылается наш делегат. В завершение приведу пример, который будет аккумулировать сказанное ранее и наглядно демонстрировать работу с простейшим делегатом.

В результате работы приложения на консоль будет выведена следующая строка:

Возникает естественный вопрос: «А к чему, собственно, такие сложности, не проще ли напрямую обратиться к методу MyHandler?» Оказывается, не проще. Хотя для данного примера, действительно, проще, поскольку мы точно знаем, какой метод нам нужно вызвать. Но для проектирования обратной связи с внешними системами делегаты незаменимы. Мы просто передаем внешней системе делегат, ссылающийся на наш внутренний метод. И покорно ждем, когда внешняя система обратится к нашей внутренней функции через переданный делегат.

Делегат и экземплярные методы

Теперь рассмотрим такой важный вопрос, как вызов экземплярных методов. Единственным отличием от обращения к статическим функциям будет способ создания делегата. Необходимо будет указать ссылку на объект, который будет использоваться при вызове метода через данный делегат.

Где sc является ссылкой на экземпляр объекта, который должен использоваться при обращении к методу MyHandler.

Приведу пример, демонстрирующий работу с подобными делегатами. Дабы излишне не запутывать читателя, а также упростить чтение листинга примера, в него введен дополнительный класс. В классе описан метод, на который будет ссылаться делегат. Данный метод будет использовать поле класса, что позволит нам следить за работой метода, изменяя его (листинг 3).

В результате работы приложения на консоль будут выведены следующие строки.

Видно, что метод действительно был вызван от нужного объекта. Изменения, внесенные в объект после первого обращения к методу, естественно, отразились на втором обращении к нему.

Теперь, после того как мы умеем пользоваться делегатами и знаем, для чего они нужны, приступим к их более детальному изучению.

MulticastDelegate

Этот класс является неотъемлемым в жизни любого делегата. Он предоставляет основные сервисы по управлению делегатами. Хотя многие из рассматриваемых здесь его членов все же описаны в базовом классе Delegate. Но в данном случае это можно отнести к конструкционным особенностям и не обращать на них внимания. Ниже, в таблице 1, приведено краткое описание наиболее важных членов класса MulticastDelegate.

Свойства

Method Возвращает метод, на который ссылается делегат
Target Возвращает объект, к которому привязан метод, на который ссылается делегат

Методы

DynamicInvoke Позволяет динамически обратиться к функциям, связанным с делегатом
GetInvocationList Возвращает массив делегатов, привязанных к делегату, в порядке, в котором они вызываются.
Equality Operator Оператор (==), позволяет определить равенство делегатов
Inequality Operator Оператор (!=), позволяет определить, различны ли делегаты.
Combine Конкатенирует два (или более) делегата, создавая новый делегат, список вызовов которого включает списки объединяемых делегатов. Исходные делегаты не модифицируются.
Remove Удаляет список вызовов одного делегата из списка вызовов другого. При этом создается новый делегат, список вызовов которого представляет собой результат удаления. Исходные делегаты не модифицируются.
CreateDelegate Позволяет динамически создать делегат
Таблица 1. Описание членов класса MulticastDelegate.

Обратите внимание, что многие из методов описаны как статические. Это очень важный момент, который мы рассмотрим несколько позднее.

MulticastDelegate.Method

Возвращает описание метода, на который ссылается делегат.

Особенность свойства состоит в том, что если делегат содержит не одну ссылку на функцию, а несколько, то свойство всегда будет возвращать последнюю добавленную ссылку.

MulticastDelegate.Target

Возвращает объект, с которым связан метод, на который ссылается делегат.

Если делегат ссылается на статический метод, свойство вернет пустую ссылку (null). Особенность свойства состоит в том, что если делегат содержит не одну, а несколько ссылок на методы, то свойство вернет объект, связанный с последним из добавленных в очередь методов.

Пример использования свойств Method и Target

Для наглядной демонстрации возможностей рассмотренных свойств, приведу пример, использующий их (листинг 4). В нем будет объявлен простой тестовый класс (SomeClass), содержащий единственный экземплярный метод (InstanceMethod). В программе будет создан объект данного класса. Далее будет создан делегат и к нему присоединен метод InstanceMethod. Затем на консоль будет выведена информация о делегате, полученная при помощи свойств Method и Target.

В результате работы приложения на консоль будут выведены следующие строки.

В случае, когда делегат содержит ссылку только на один метод, использование этих свойств крайне примитивно. Если же делегат содержит более одной ссылки, такой подход к получению информации годиться не будет. Необходимо будет воспользоваться другими способами. Оба свойства будут возвращать информацию только по последнему методу, добавленному в список вызова делегата.

При использовании свойства Target необходимо помнить, что статические методы не имеют привязки к объектам и значение этого свойства всегда будет равно нулевой ссылке.

MulticastDelegate.DynamicInvoke

Метод позволяет динамически обратиться к делегату.

Практическая польза метода весьма сомнительна, тем не менее, приведем пример работы с ним. Чтобы разнообразить пример, попытаемся вызвать делегат с неверным количеством параметров и посмотрим, что получится (листинг 5).

В результате работы приложения сначала на консоль будет выведена строка.

Затем будет выведено сообщение о возникшем исключении TargetParameterCountException, со следующим объяснением

То есть нам сообщают, что при вызове делегата было передано неверное количество аргументов. Таким образом, при работе с методом DynamicInvoke необходимо иметь в виду, что могут произойти неприятности подобного рода.

Операторы сравнения

(Equality и Inequality)

Операторы позволяют выяснить, ссылаются ли два делегата на одну и ту же функцию.

При сравнении делегатов учитывается не только метод, на который ссылается делегат, но и ссылка на объект, с которым связан метод.

MulticastDelegate.Combine и MulticastDelegate.Remove

Методы предназначены для поддержки делегатов, ссылающихся одновременно на несколько функций. Первый метод позволяет объединить несколько делегатов в один, в списке вызовов которого будут находиться списки вызовов всех скомбинированных делегатов.

Обратите внимание на то, что в результате работы метода будет возвращен новый делегат, который в своем списке вызовов будет содержать функции всех указанных делегатов. При этом старые делегаты при выполнении данной операции абсолютно не пострадают, их списки вызова останутся нетронутыми.

Наряду с возможностью объединения нескольких делегатов в один, существует обратная операция — изъятия указанных методов из списка вызовов некоторого делегата.

Рассмотренный метод, также как и метод Combine, не изменяет значение параметров, переданных ему. Он создает абсолютно новый делегат, в который помещает результаты своей работы.

Разработчики среды .NET решили, что простым программистам будет затруднительно использовать эти методы, в силу их нетривиальности. Поэтому в компилятор C# были введены специальные средства поддержки делегатов. Вы можете оперировать с делегатами при помощи обычных операций сложения и вычитания (+,-,+=,-=). При этом компилятор автоматически будет генерировать код, обращающийся к методам Combine и Remove.

Для наглядности приведем пример, демонстрирующий работу с делегатами, ссылающихся на несколько методов (листинг 6).

В результате работы приложения на консоль будут выведены следующие строки.

Обратите внимание, что функция была вызвана два раза, хотя мы добавляли ее в список вызова делегата три раза. Но если учесть, что мы один раз изъяли ее из списка вызова, то все становится понятно. Интересно, что в списке вызова делегата все методы одинаковы, но этот факт для сервисов, оперирующих с делегатами, не имеет никакого значения.

Компилятор преобразует арифметические операции над делегатами в обращения к методам Combine и Remove. Чтобы наглядно продемонстрировать это, приведу листинг функции Main на языке IL, который получился после транслирования исходного кода компилятором языка C# (листинг 7).

Понимая, что чтение IL-листингов может быть для некоторых читателей несколько утомительно, приведем код примера на языке C#, напрямую использующего методы Combine и Remove (листинг 8).

Результат работы данного примера нисколько не будет отличаться от предыдущего.

Таким образом, использование методов Combine, Remove и арифметических операций, предоставляемых компилятором, по сути дела, ничем не отличается. Разве что обращение ко вторым более удобно, и программы, написанные с их использованием, читаются гораздо легче.

Delegate.Invoke или что там внутри?

Напомним, как выглядит механизм вызова делегата через обращение к методу Invoke.

Программистам, использующим языки высокого уровня, это может показаться странным, поскольку на их уровне обращение к делегату происходит как к обычной функции.

https://amdy.su/wp-admin/options-general.php?page=ad-inserter.php#tab-8

Дело в том, что метод Invoke недокументирован. Он не является членом классов Delegate и MulticastDelegate. Соответственно, можно предположить, что это специальный метод, генерируемый компилятором. Для проверки предположения обратимся к IL-коду. Действительно, там обнаружится метод Invoke, его код приведен ниже.

Его изучение показывает, что метод лишен какого-либо кода. Он попросту пустой и ничего не может делать. Так может показаться, если не принимать во внимание спецификатор runtime, используемый при определении метода. Данный спецификатор указывает на то, что метод будет реализован по ходу исполнения программы самой средой исполнения. Это означает, что код, обрабатывающий вызов метода, располагается в недрах самой среды исполнения. А она прекрасно осведомлена о внутреннем устройстве делегата и справится с вызовом всех его методов без дополнительной помощи со стороны программиста.

Помимо этого можно отметить, что конструктор каждого делегата описывается подобным образом. Для примера приведем код конструктора одного из делегатов.

Код, реализующий делегат, также находится внутри среды исполнения. Оказывается, метод Invoke отвечает не только за вызов делегата, но также за хранение информации о прототипе методов, на которые может ссылаться делегат. Не правда ли, изящное решение? Прототип закодирован в самом методе Invoke, то есть его прототип полностью совпадает с прототипом делегата. Таким образом, не надо прибегать к использованию дополнительных средств, вроде MethodInfo, для хранения информации о прототипе. К тому же, это страхует от возможности случайной передачи неверного количества или типа параметров при классическом обращении к делегату.

MulticastDelegate.GetInvocationList

Возвращает список делегатов, находящихся в списке вызовов делегата.

Несмотря на кажущуюся абстрактность метода, он имеет очень важное практическое применение.

Существует два недостатка механизма вызова функций, связанных с делегатом. И оба они связаны со случаем, когда в списке вызова делегата присутствует более одной функции.

  • Функции, на которые ссылаются делегаты, могут возвращать значения. Но мы их получить не сможем, поскольку штатная функция вызова Invoke возвращает значение, которая вернула последняя из опрошенных функций.
  • При обращении к делегату, содержащему в списке вызова несколько функций, вовсе не гарантируется, что все функции из списка будут вызваны. Если одна из них сгенерирует исключение, то работа метода Invoke будет прервана и остальные функции вызваны не будут.

Особое волнение вызывает вторая проблема, поскольку важные сообщения попросту могут не дойти до адресата. Что неприемлемо при разработке надежных систем. Рассмотрим пример (листинг 9).

В результате работы программы на консоль будут выведены следующие строки.

Таким образом, исходя из информации, представленной в логе, нетрудно понять, что в список вызова делегата было добавлено два метода, но вызван был только первый из них. Надеюсь, причины такого поведения программы понятны.

Обойти проблему можно следующим образом. Для этого придется воспользоваться методом GetInvocationList, чтобы организовать обращение к делегатам полностью в ручном режиме. Преимущество подхода заключается в том, что можно полноценно контролировать обращение к каждому методу, на который ссылается делегат. Код примера вы обнаружите в листинге 10.

В результате работы программы на консоль будут выведены следующие строки.

Видно, что задачи, поставленные перед приложением, были успешно выполнены. Несмотря на то, что в одной из функций, на которую ссылался делегат, было выброшено исключение, это не отразилось на работе остальных функций.

Помимо кода, обрабатывающего исключения, можно добавить специальные сервисы, анализирующие значения, возвращаемые функциями, вызываемыми через делегат.

Отметим, что обращаться к методам можно не только через функцию DynamicInvoke. Для этих целей также можно использовать свойство Method. А точнее, метод Invoke, предоставляемый классом MethodInfo, экземпляр которого можно получить через вышеупомянутое свойство класса Delegate. В итоге, код вызова можно заменить следующим:

Особую практическую пользу в этом подходе найти, конечно, сложно, особенно учитывая его медлительность. Но зато вы будете всегда иметь запасной вариант при работе с делегатами.

MulticastDelegate.CreateDelegate

Метод позволяет динамически создать делегат заданного типа.

Этот метод введен в рамках поддержки технологии отражения, он позволяет динамически создавать делегаты произвольного типа. Он используется, если на момент компиляции не известно, сколько аргументов принимает метод обратного вызова и какого они типа.

«Нулевые делегаты»

При программировании делегатов может создаться впечатление, что существуют так называемые нулевые делегаты. Под нулевыми делегатами подразумеваются экземпляры классов делегатов, не содержащие ссылок на методы. Такой делегат можно попытаться получить, изъяв из списка все методы, на которые он ссылается при помощи функции Remove. В коде это будет выглядеть так.

И если до исполнения инструкции делегат ссылался на один метод SomeMethod, то в результате ее исполнения мы должны получить пустой делегат. На самом деле, мы получаем нулевую ссылку, то есть просто null. Что, кстати говоря, неочевидно, поскольку мы и далее имеем переменную del, которая подразумевает под собой ссылку на экземпляр делегата.

Большинство программистов попросту не ожидают такого подвоха со стороны системы и попадаются в коварную ловушку — они пытаются использовать методы данного экземпляра делегата. А поскольку он равен null, то происходит исключение NullReferenceException. Для того чтобы избежать подобных ошибок, в неоднозначных местах программы необходимо обязательно проверять значение делегата на null. Делается это следующим образом.

Во всех случаях, когда значение делегата не известно со стопроцентной уверенностью, такая проверка обязательно должна присутствовать. Это позволит вам избежать очень неприятных ошибок.

События

В разделе будут рассмотрены члены типов — события.

Как устроены события и зачем они нужны

Начнем изучение с несколько идеализированного примера. Допустим, имеется некоторый компонент, и пускай для упрощения примера это будет банальная кнопка. Всем известно, что на кнопки можно нажимать. При нажатии кнопки происходит событие «щелчок», о котором необходимо обязательно уведомлять пользователя нашего компонента. Для этого введем в класс, представляющий компонент-кнопку, общедоступное поле Click, являющееся экземпляром делегата. И каждый раз, когда будет происходить соответствующее событие, будем обращаться к этому делегату. А он, в свою очередь, будет вызывать прикрепленные к нему функции и методы. Приведу код (листинг 11).

Теперь пользователи нашего класса смогут присоединить свои функции к переменной-полю экземпляра делегата и получить уведомление о произошедшем событии.

Но поскольку среда .NET является объектно-ориентированной, мы обязаны соблюдать правила инкапсуляции полей. Соответственно, необходимо ввести дополнительные методы, обслуживающие поле-экземпляр делегата и контролирующие все производимые над ним операции, а само поле полагается сделать закрытым. Сделаем это следующим образом — добавим две функции add_Click и remove_Click, которые, соответственно, будут добавлять и убирать события в очередь делегата. Новый код представлен в листинге 12.

Теперь наш компонент удовлетворяет основным парадигмам объектно-ориентированного программирования. Только вот, его код стал уж больно громоздким и его использование не будет столь наглядным, как прежде.

События .NET

Понимали это и разработчики, создававшие среду .NET. Для решения этой задачи они ввели специальное поле-событие, которое автоматически вводит поле-делегат и два дополнительных метода, обслуживающих его. Таким образом, автоматически реализуется представленная выше модель.

В языке высокого уровня C# определение таких полей осуществляется при помощи ключевого слова event. Также компилятор берет на себя заботу о работе с этими полями-событиями, благодаря чему к ним можно прибавлять и вычитать делегаты, хотя они, по сути дела, таковыми не являются. Приведем наглядный пример использования полей-событий (листинг 13).

В результате работы приложения, на консоль будет выведена следующая строка:

Таким образом, работа с событиями представляется предельно простой. Она, по сути дела, ничем не отличается от взаимодействия с обыкновенными делегатами.

Внутренний механизм поддержки событий

Рассмотрим, какой код поддержки события был создан компилятором. Для этого изучим IL- код.

Во-первых, компилятор создал поле-делегат, в котором хранятся все зарегистрированные обработчики события.

Обратите внимание, что поле является закрытым и может быть использовано только из самого класса, исключая даже его потомков (private). Также компилятор создал одноименное специализированное свойство, в котором указаны методы, реализующие внешнюю работу с данным событием. Причем необходимо отметить, что это свойство по умолчанию является общедоступным.

Дабы не утомлять читателя излишним чтением IL-листингов, приведем код методов на языке C#.

Методы будут использоваться только при работе с событием извне класса. Внутри же используется прямое обращение к полю Click, что несколько быстрее, чем вызов методов add_Click и remove_Click. Здесь можно усмотреть борьбу команды разработчиков компилятора С# за производительность создаваемых им программ.

Чтобы подтвердить сказанное, приведем IL-код функции SimulateClick, которая непосредственно обращается к событию из самого класса, если быть до конца точным, то не к самому событию, а к полю-экземпляру делегата.

Обратите внимание на выделенную в коде инструкцию ldfld — она предназначена для работы с полями объекта. Также в листинге отсутствуют обращения к функциям add_Click и remove_Click. Что и требовалось доказать!

Контроль над событиями

При разборе первого примера подраздела говорилось, что введение дополнительных функций позволяет контролировать работу с событием. Но как оказалось, события .NET полностью закрыты для программиста. Они скрывают всю грязную работу за кулисами и контролировать обращение к событиям не представляется возможным. Но это утверждение верно только при стандартном способе использования событий. Также существует дополнительный расширенный режим их использования, при котором программист может самостоятельно объявить функции, управляющие подключением и отключением делегатов-обработчиков. Для этого в языке C# предусмотрена специальная конструкция, с использованием двух дополнительных ключевых слов add и remove.

Единственное, что может смутить при рассмотрении кода, так это то, что add и remove, по сути дела, являются функциями, принимающими один параметр. Чтобы получить доступ к этому параметру, необходимо воспользоваться ключевым словом value.

При использовании расширенного режима компилятор не будет автоматически описывать поле-делегат, и его придется вводить самостоятельно. Рассмотрим пример, демонстрирующий работу данного подхода (листинг 14). Этот пример по функциональности и внутренней архитектуре будет аналогичен предыдущему, только код поддержки работы с событием будет реализован в нем вручную.

В результате работы примера получим на консоли следующие строки.

Как видим, приложение действовало по заданной схеме, контролируя при этом добавление и изъятие делегатов.

Такая возможность может быть весьма полезна для анализа и слежения за функциями, прикрепленными к событию. К примеру, можно запретить добавление событий кодом, не имеющим определенных привилегий защиты.

Дополнительные возможности при работе с делегатами

Здесь будут рассмотрены дополнительные возможности, предоставляемые общей библиотекой классов (FCL, Framework Class Library) для упрощения работы с делегатами. Некоторые из них могут быть весьма полезны в повседневном программировании.

Список делегатов — EventHandlerList

В рамках компонентной модели общей библиотеки классов введен дополнительный класс EventHandlerList. Он предназначен для упрощения разработки компонентов, содержащих большое количество событий. Класс позволяет хранить в своем экземпляре неограниченное количество событий, организуя доступ к ним при помощи произвольных ключей типа Object. Наиболее интересные члены класса EventHandlerList описаны в таблице 2.

AddHandler Добавляет делегат в список по ключу
RemoveHanlder Изымает делегат из списка по ключу
Таблица 2. Члены класса System.ComponentModel.EventHandlerList.

По сути дела, список является и не списком вовсе, а ассоциативным массивом. Но основное его достоинство заключается не в этом. Главное его отличие от обычных коллекций состоит в том, что он производит добавление и изъятие элементов из списка, учитывая особенности делегатов. Операции добавления и изъятия производятся при помощи методов Combine и Remove. Таким образом, по одному ключу может храниться несколько скомбинированных однотипных делегатов. Соответственно, при изъятии делегата по заданному ключу, элемент списка будет удален не полностью, а произойдет рекомбинация делегата, в ходе которой некоторые из его ссылок будут потеряны. Но сам делегат может не истощиться, а содержать еще несколько ссылок. Такое поведение списка весьма удобно при разработке компонентов, предоставляющих пользователям множество событий. Можно попросту добавлять и удалять делегаты по заданным ключам, не задумываясь о дополнительной поддержке событий.

Продемонстрируем работу со списком на примере того же компонента кнопки (листинг 15). На этот раз он будет содержать два события, делегаты которых будут храниться в специализированном закрытом списке.

В результате работы приложения на консоль будут выведены следующие строки.

Как видно, нововведение никоим образом не отразилось на использовании класса извне. Была всего лишь изменена внутренняя организация компонента, несколько упростив работу программистам при помощи специализированного хранилища делегатов.

Стандартный делегат общей библиотеки

На протяжении главы всегда использовался собственноручно определенный делегат следующего вида:

Многие классы стандартной библиотеки используют делегаты для уведомления о произошедших в них событиях. Соответственно, разработчики среды .NET сочли, что будет целесообразно ввести общий стандартный тип делегата, который будет использоваться во всей библиотеке. Его прототип представлен ниже.

Этот делегат принимает всего лишь два параметра, что явно маловато для универсального делегата. Но все же оказывается, параметров с лихвой хватает для передачи любой информации. Сам по себе класс EventArgs не содержит ни одного интересного члена, способного передавать какую бы то ни было информацию. Он введен лишь для обобщения. Когда необходимо передать дополнительную информацию, вводится новый класс, производный от EventArgs, в котором уже и вводятся поля, передающие необходимую информацию. В одной общей библиотеке у класса EventArgs 100 потомков, и их количество от версии к версии среды исполнения неуклонно растет.

При обращении к событию, имеющему тип данного делегата, в качестве параметра необходимо передать ссылку на текущий объект (this), а во втором параметре экземпляр класса EventArgs или производного от него.

где свойство Empty попросту возвращает пустой экземпляр типа EventArgs. В принципе, его можно создать и самому воспользовавшись оператором new.

Правда, такой подход будет работать несколько медленнее, чем первый, поскольку здесь появилась дополнительная операция создания объекта. И причем необходимо отметить, что она далеко из не самых быстродействующих.

В заключение

Использование делегатов и событий в обычных управляемых приложениях несложно. Выше были рассмотрены все наиболее важные аспекты проблемы. Но при проектировании производительных и устойчивых приложений, можно задуматься о создании собственного механизма обращения к делегатам, с использованием многопоточной модели и более защищенных алгоритмов.

Делегаты и события в C#

Перевод статьи подготовлен специально для студентов курса «Разработчик С#».

Что такое события в C#?

Понимание делегатов в C#

В C# делегаты образуют основные строительные блоки для событий. Делегат — это тип, который определяет сигнатуру метода. Например, в C++ это можно сделать с помощью указателя на функцию. В C# вы можете создать экземпляр делегата, указывающий на другой метод. Вы можете вызвать этот метод через экземпляр делегата.

Ниже приведен пример объявления делегата и вызова метода через него.

Использование делегата в C#

Как видите, мы используем ключевое слово delegate, чтобы сообщить компилятору, что мы создаем тип делегата.

Инстанцировать делегаты легко вместе с автоматическим созданием нового типа делегата.

Для создания делегата вы также можете использовать ключевое слово new.

MathDelegate mathDelegate = new MathDelegate(Add) ;

Инстанцированный делегат является объектом; Вы можете также использовать его и передавать в качестве аргумента другим методам.

Многоадресные делегаты в C#

Еще одна замечательная особенность делегатов в том, что вы можете объединять их вместе. Это называется многоадресной передачей (multicasting). Вы можете использовать оператор + или +=, чтобы добавить другой метод в список вызовов существующего экземпляра делегата. Аналогично, вы также можете удалить метод из списка вызовов, используя оператор присваивания декремента (- или -=). Эта особенность служит основой для событий в C#. Ниже приведен пример многоадресного делегата.

Это возможно, поскольку делегаты наследуются от класса System.MulticastDelegate , который, в свою очередь, наследуется от System.Delegate . Вы можете использовать члены, определенные в этих базовых классах для ваших делегатов.

Например, чтобы узнать, сколько методов будет вызывать многоадресный делегат, вы можете использовать следующий код:

Ковариантность и контравариантность в C#

Когда вы назначаете делегату метод, сигнатура метода не обязательно должна точно соответствовать делегату. Это называется ковариацией и контравариантностью. Ковариация позволяет методу иметь более производный тип возвращаемого значения, нежели тот, который определен в делегате. Контравариантность разрешает метод с типами параметров, которые являются менее производными, чем типы в делегате.

Ковариация в делегатах

Вот пример ковариации,

Поскольку и StreamWriter , и StringWriter наследуются от TextWriter , вы можете использовать CovarianceDel с обоими методами.

Контравариантность в делегатах

Ниже приведен пример контравариантности.

Поскольку метод DoSomething может работать с TextWriter , он, безусловно, может работать и с StreamWriter . Благодаря контравариантности вы можете вызывать делегат и передавать экземпляр StreamWriter в метод DoSomething .

Вы можете узнать больше об этой концепции здесь.

Лямбда-выражения в C#

Иногда вся сигнатура метода может требовать больше кода, чем само тело метода. Существуют также ситуации, в которых вам нужно создать целый метод только для того, чтобы использовать его в делегате.

Для этих случаев Microsoft добавила некоторые новые возможности в C#, например, анонимные методы в 2.0. В C# 3.0 дела стали обстоять еще лучше, когда были добавлены лямбда-выражения. Лямбда-выражение является предпочтительным способом при написании нового кода.

Ниже приведен пример новейшего лямбда-синтаксиса.

Для чтения этого кода вам нужно использовать слово “следует” в контексте специального лямбда-синтаксиса. Например, первое лямбда-выражение в вышеприведенном примере читается как «x и y следуют к сложению x и y».

Лямбда-функция не имеет конкретного имени в отличии от метода. Из-за этого лямбды называются анонимными функциями. Вам также не нужно явно указывать тип возвращаемого значения. Компилятор предполагает его автоматически из вашей лямбды. И в случае вышеприведенного примера типы параметров x и y также не указаны явно.

Вы можете создавать лямбды, которые охватывают несколько операторов. Вы можете сделать это, добавив фигурные скобки вокруг операторов, которые образуют лямбду, как показано в примере ниже.

Иногда объявление делегата для события кажется немного громоздким. Из-за этого .NET Framework имеет несколько встроенных типов делегатов, которые вы можете использовать при объявлении делегатов. В примере MathDelegate вы использовали следующий делегат:

Вы можете заменить этот делегат одним из встроенных типов, а именно Func <int, int, int> .

Типы Func <. > можно найти в пространстве имен System. Они представляют делегаты, которые возвращают тип и принимают от 0 до 16 параметров. Все эти типы наследуются от System.MulticaseDelegate для того, чтобы вы могли добавить несколько методов в список вызовов.

Если вам нужен тип делегата, который не возвращает значение, вы можете использовать типы System.Action. Они также могут принимать от 0 до 16 параметров, но не возвращают значение.

Вот пример использования типа Action,

Вы можете узнать больше о встроенных делегатах .NET здесь.

Все усложняется, когда ваша лямбда-функция начинает ссылаться на переменные, объявленные вне лямбда-выражения или на this. Обычно, когда элемент управления покидает область действия переменной, переменная становится недействительной. Но что, если делегат ссылается на локальную переменную. Чтобы исправить это, компилятор генерирует код, который продлевает срок службы захваченной переменной, по крайней мере, до тех пор, пока живет самый долгоживущий делегат. Это называется замыканием.

Вы можете узнать больше о замыканиях здесь.

События в C#

Рассмотрим популярный шаблон разработки — издатель-подписчик (pub/sub). Вы можете подписаться на событие, а затем вы будете уведомлены, когда издатель события инициирует новое событие. Эта система используется для установления слабой связи между компонентами в приложении.

Делегат формирует основу для системы событий в C#.

Событие — это особый тип делегата, который облегчает событийно-ориентированное программирование. События — это члены класса, которые нельзя вызывать вне класса независимо от спецификатора доступа. Так, например, событие, объявленное как public, позволило бы другим классам использовать += и -= для этого события, но запуск события (то есть вызов делегата) разрешен только в классе, содержащем событие. Давайте посмотрим на пример,

Затем метод в другом классе может подписаться на событие, добавив один из его методов в делегат события:

Ниже приведен пример, показывающий, как класс может предоставить открытый делегат и генерировать событие.

Даже если событие объявлено как public , оно не может быть запущено напрямую нигде, кроме как в классе, в котором оно находится.

Используя ключевое слово event , компилятор защищает наше поле от нежелательного доступа.

Он не позволяет использовать = (прямое назначение делегата). Следовательно, ваш код теперь защищен от риска удаления предыдущих подписчиков, используя = вместо +=.

Кроме того, вы могли заметить специальный синтаксис инициализации поля OnChange для пустого делегата, такого как delegate < >. Это гарантирует, что наше поле OnChange никогда не будет null. Следовательно, мы можем удалить null-проверку перед тем, как вызвать событие, если нет других членов класса, делающих его null.

Когда вы запускаете вышеуказанную программу, ваш код создает новый экземпляр Pub, подписывается на событие двумя разными методами и генерирует событие, вызывая p.Raise. Класс Pub совершенно не осведомлен ни об одном из подписчиков. Он просто генерирует событие.

Вы также можете прочитать мою статью «Шаблон проектирования издатель-подписчик в C#» для более глубокого понимания этой концепции.

Что ж, на этом пока это все. Надеюсь, вы уловили идею. Спасибо за прочтение поста. Пожалуйста, дайте мне знать, если есть какие-либо ошибки или необходимы изменения в комментарии ниже. Заранее спасибо!

Что такое делегат в языке С#?

Объясните простым, человеческим языком, кто такой и зачем нужен делегат в ООП вообще и в С# в частности?

Kromster's user avatar

Сергей's user avatar

Делегат — это объектно-ориентированный способ работы с методом как с переменной. Его более привычный аналог — указатель на функцию, функтор, или даже просто вектор прерывания (спасибо @rdorn за подсказку).

Делегат представляет собой тип, соответствующий определённой сигнатуре функции. Объявив переменную делегатного типа, вы можете записать в неё статический или нестатический метод, передать его как аргумент куда-либо, и вызвать.

Классический пример использования делегатов — сортировка списка объектов по значению какого-либо поля. Вы передаёте в сортирующий метод делегат, который по объекту вычисляет ключ сортировки, то есть, вытаскивает значение поля.

VladD's user avatar

Делегат — это тип, представляющий ссылки на методы с конкретным списком параметров и возвращаемым типом. При создании экземпляра делегата этот экземпляр можно связать с любым методом с совместимой сигнатурой и возвращаемым типом. Метод можно вызвать (активировать) с помощью экземпляра делегата.

Добавлю своими словами: делегаты можно назвать особыми типами с кодом, генерируемым на этапе выполнения. Также можно сказать, что это своего рода указатели на методы. Вызов делегатов выполняется в том же потоке через метод, вызванный с использованием делегата, причем исключения бросаются так же, как будто метод был вызван напрямую. Вызов делегата с одним методом равносилен обычному вызову метода. Один делегат может вызвать много методов (если подписаны на несколько методов). Касаемо ООП — как уже было сказано, это объектно-ориентированный способ работы с методом. Вообще, тема ООП достаточно большая, поэтому тут только вскользь. Делегат — это такой же тип, как к примеру класс или интерфейс, он может определяться либо внутри класса, либо вне, в пределах пространства имен.

На ruSO C#-делегаты достаточно популярная тема, вопросы и ответы на которую имеются, для изучения приведу пару примеров:

Denis Bubnov's user avatar

Уже 2 ответа даны. Я добавлю третий вариант, более человечный 🙂

Делегат — лучше всего вспомнить о таком слове как Представитель. Это слово будет синонимом или, если хотите, переводом на русский яз. Еще можно вспомнить о слове Депутат. Короче, представитель, который представляет в качестве себя одного любимого многих. Депутат представляет выборщиков, много выборщиков. Делегат в языке C# — представляет или может представлять много методов. Этакое отношение один-ко-многим.

Делегат похож на переменную, которая может ссылаться на один или больше методов с одинаковой сигнатурой и возвращаемым типом. После создания делегата с нужной сигнатурой, вы можете подписывать (присоединять к списку вызова этого делегата с помощью операции += ) методы или отписывать (удалять из списка вызова операцией -= ) методы. В дальнейшем использовав вызов делегата, вы производите вызов всех методов, которые ранее были в списке вызова. Еще одна аналогия — список e-mail рассылки — отправка одного письма сразу многим адресатам.

Про использование в ООП: на делегатах основаны события, с помощью делегатов можно создавать анонимные методы (методы без имени), ну и см. ответ уважаемого @VladD.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *