Делегаты. Анонимные методы
Делегаты
- Делегаты представляют такие объекты, которые указывают на методы. То есть делегаты - это указатели на методы и с помощью делегатов мы можем вызвать данные методы.
Определение делегатов
- Для объявления делегата используется ключевое слово delegate , после которого идет возвращаемый тип, название и параметры. Например:
- Делегат Message в качестве возвращаемого типа имеет тип void (то есть ничего не возвращает) и не принимает никаких параметров. Это значит, что этот делегат может указывать на любой метод, который не принимает никаких параметров и ничего не возвращает.
- Рассмотрим применение этого делегата:
Прежде всего сначала необходимо определить сам делегат:
Для использования делегата объявляется переменная этого делегата:
- Далее в делегат передается адрес определенного метода (в нашем случае метода Hello). Обратите внимание, что данный метод имеет тот же возвращаемый тип и тот же набор параметров (в данном случае отсутствие параметров), что и делегат.
- Затем через делегат вызываем метод, на который ссылается данный делегат:
- Вызов делегата производится подобно вызову метода. При этом делегаты необязательно могут указывать только на методы, которые определены в том же классе, где определена переменная делегата. Это могут быть также методы из других классов и структур.
Место определения делегата
- Если мы определяем делегат в программах верхнего уровня (top-level program), которую по умолчанию представляет файл Program.cs начиная с версии C# 10, как в примере ранее, то, как и другие типы, делегат определяется в конце кода. Но в принципе делегат можно определять внутри класса:
Параметры и результат делегата
- Рассмотрим определение и применение делегата, который принимает параметры и возвращает результат:
В данном случае делегат Operation возвращает значение типа int и имеет два параметра типа int.
Поэтому этому делегату соответствует любой метод, который возвращает значение типа int и принимает два
параметра типа int. В данном случае это методы Add и Multiply. То есть мы можем присвоить переменной делегата
любой из этих методов и вызывать.
Поскольку делегат принимает два параметра типа int, то при его вызове необходимо передать значения
для этих параметров: operation(4,5) .
Присвоение ссылки на метод
- Выше переменной делегата напрямую присваивался метод. Есть еще один способ - создание объекта делегата с помощью конструктора, в который передается нужный метод:
Оба способа равноценны .
Соответствие методов делегату
- Как было написано выше, методы соответствуют делегату, если они имеют один и тот же возвращаемый тип и один и тот же набор параметров. Но надо учитывать, что во внимание также принимаются модификаторы ref , in и out . Например, пусть у нас есть делегат:
- Этому делегату соответствует, например, следующий метод:
- А следующие методы НЕ соответствуют:
Здесь метод SomeMethod2 имеет другой возвращаемый тип, отличный от типа делегата. SomeMethod3 имеет другой набор параметров. Параметры SomeMethod4 и SomeMethod5 также отличаются от параметров делегата, поскольку имеют модификаторы ref и out.
Добавление методов в делегат
- В примерах выше переменная делегата указывала на один метод. В реальности же делегат может указывать на множество методов, которые имеют ту же сигнатуру и возвращаемые тип. Все методы в делегате попадают в специальный список - список вызова или invocation list. И при вызове делегата все методы из этого списка последовательно вызываются. И мы можем добавлять в этот список не один, а несколько методов.
- Для добавления методов в делегат применяется операция += :
- В данном случае в список вызова делегата message добавляются два метода - Hello и HowAreYou. И при вызове message вызываются сразу оба этих метода.
- Однако стоит отметить, что в реальности будет происходить создание нового объекта делегата, который получит методы старой копии делегата и новый метод, и новый созданный объект делегата будет присвоен переменной message.
- При добавлении делегатов следует учитывать, что мы можем добавить ссылку на один и тот же метод несколько раз, и в списке вызова делегата тогда будет несколько ссылок на один и то же метод. Соответственно при вызове делегата добавленный метод будет вызываться столько раз, сколько он был добавлен:
- Подобным образом мы можем удалять методы из делегата с помощью операций -= :
При удалении методов из делегата фактически будет создаваться новый делегат, который в списке вызова методов будет содержать на один метод меньше.
Стоит отметить, что при удалении метода может сложиться ситуация, что в делегате не будет методов, и тогда переменная будет иметь значение null. Поэтому в данном случае переменная определена не просто как переменная типа Message , а именно Message?, то есть типа, который может представлять как делегат Message, так и значение null.
- Кроме того, перед вторым вызовом мы проверяем переменную на значение null.
- При удалении следует учитывать, что если делегат содержит несколько ссылок на один и тот же метод, то операция -= начинает поиск с конца списка вызова делегата и удаляет только первое найденное вхождение. Если подобного метода в списке вызова делегата нет, то операция -= не имеет никакого эффекта.
Объединение делегатов
- Делегаты можно объединять в другие делегаты. Например:
- В данном случае объект mes3 представляет объединение делегатов mes1 и mes2. Объединение делегатов значит, что в список вызова делегата mes3 попадут все методы из делегатов mes1 и mes2. И при вызове делегата mes3 все эти методы одновременно будут вызваны.
Вызов делегата
- В примерах выше делегат вызывался как обычный метод. Если делегат принимал параметры, то при его вызове для параметров передавались необходимые значения:
- Другой способ вызова делегата представляет метод Invoke() :
- Если делегат принимает параметры, то в метод Invoke передаются значения для этих параметров.
- Следует учитывать, что если делегат пуст, то есть в его списке вызова нет ссылок ни на один из методов (то есть делегат равен Null), то при вызове такого делегата мы получим исключение , как, например, в следующем случае:
- Поэтому при вызове делегата всегда лучше проверять, не равен ли он null. Либо можно использовать метод Invoke и оператор условного null :
- Если делегат возвращает некоторое значение, то возвращается значение последнего метода из списка вызова (если в списке вызова несколько методов). Например:
Обобщенные делегаты
- Делегаты, как и другие типы, могут быть обобщенными , например:
- Здесь делегат Operation типизируется двумя параметрами типов. Параметр T представляет тип возвращаемого значения. А параметр K представляет тип передаваемого в делегат параметра. Таким образом, этому делегату соответствует метод, который принимает параметр любого типа и возвращает значение любого типа.
В прогамме мы можем определить переменные делегата под определенный метод. Например, делегату
Operation соответствует метод, который принимает число int и возвращает число типа decimal.
А делегату Operation соответствует метод, который принимает и возвращает число типа int.
Делегаты как параметры методов
- Также делегаты могут быть параметрами методов. Благодаря этому один метод в качестве параметров может получать действия - другие методы. Например:
- Здесь метод DoOperation в качестве параметров принимает два числа и некоторое действие в виде делегата Operation. В внутри метода вызываем делегат Operation, передавая ему числа из первых двух параметров.
- При вызове метода DoOperation мы можем передать в него в качестве третьего параметра метод, который соответствует делегату Operation.
Возвращение делегатов из метода
- Также делегаты можно возвращать из методов. То есть мы можем возвращать из метода какое-то действие в виде другого метода. Например:
В данном случае метод SelectOperation() в качестве параметра принимает перечисление типа OperationType. Это перечисление хранит три константы, каждая из которых соответствует определенной
арифметической операции. И в самом методе в зависимости от значения параметра возвращаем определенный метод.
Причем поскольку возвращаемый тип метода - делегат Operation, то метод должен возвратить метод, который
соответствует этому делегату - в нашем случае это методы Add, Subtract, Multiply.
То есть если параметр метода SelectOperation равен OperationType.Add ,
то возвращается метод Add, который выполняет сложение двух чисел:
- При вызове метода SelectOperation мы можем получить из него нужное действие в переменную operation:
- И при вызове переменной operation фактически будет вызываться полученный из SelectOperation метод:
Делегат Action
- Делегат Action представляет некоторое действие, которое ничего не возвращает, то есть в качестве возвращаемого типа имеет тип void :
Данный делегат имеет ряд перегруженных версий. Каждая версия принимает разное число параметров: от
Action до Action . Таким образом можно передать до 16 значений в метод.
- Как правило, этот делегат передается в качестве параметра метода и предусматривает вызов определенных действий в ответ на произошедшие действия. Например:
Делегат Predicate принимает один параметр и возвращает значение типа bool :
Predicate
- Как правило, используется для сравнения, сопоставления некоторого объекта T определенному условию. В качестве выходного результата возвращается значение true, если условие соблюдено, и false, если не соблюдено:
В данном случае возвращается true или false в зависимости от того, больше нуля число или нет.
() , то есть может принимать до 16 параметров. Func " width="640"
Еще одним распространенным делегатом является Func . Он возвращает результат действия и может принимать параметры.
Он также имеет различные формы: от Func() , где T - тип возвращаемого значения, до Func
T2,...in T16, out TResult() , то есть может принимать до 16 параметров.
Func
- Данный делегат также часто используется в качестве параметра в методах:
Метод DoOperation() в качестве параметра принимает делегат Func ,
то есть ссылку на метод, который принимает число int и возвращает также значение int.
При первом вызове метода DoOperation() ему передается ссылка на метод DoubleNumber, который увеличивает число в два раза. Во втором случае передается метод SquareNumber - опять же метод, который принимает параметр типа int и возвращает результат в виде значения int.
Здесь переменная createString представляет лямбда-выражение, которое принимает два числа int и возвращает строку, то есть представляет делегат Func .
Применение делегатов
- Однако данные примеры, возможно, не показывают истинной силы делегатов, так как нужные нам методы в данном случае мы можем вызвать и напрямую без всяких делегатов. Однако наиболее сильная сторона делегатов состоит в том, что они позволяют делегировать выполнение некоторому коду извне. И на момент написания программы мы можем не знать, что за код будет выполняться. Мы просто вызываем делегат. А какой метод будет непосредственно выполняться при вызове делегата, будет решаться потом.
- Рассмотрим подробный пример. Пусть у нас есть класс, описывающий счет в банке:
В переменной sum хранится сумма на счете. С помощью конструктора устанавливается
начальная сумма на счете. Метод Add() служит для добавления на счет, а метод Take - для снятия денег со счета.
Допустим, в случае вывода денег с помощью метода Take нам надо как-то уведомлять об этом
самого владельца счета и, может быть, другие объекты. Если речь идет о консольной программе, и
класс будет применяться в том же проекте, где он создан, то мы можем написать просто:
- Но что если наш класс планируется использовать в других проектах, например, в графическом приложении на Windows Forms или WPF, в мобильном приложении, в веб-приложении. Там строка уведомления
не будет иметь большого смысла.
- Более того, наш класс Account будет использоваться другими разработчиками в виде отдельной библиотеки классов. И эти разработчики захотят уведомлять о снятии средств каким-то другим образом, о которых мы даже можем не догадываться на момент написания класса.
- Поэтому примитивное уведомление в виде строки кода
не самое лучшее решение в данном случае. И делегаты позволяют делегировать определение действия из класса во внешний код, который будет использовать этот класс.
- Изменим класс, применив делегаты:
- Для делегирования действия здесь определен делегат AccountHandler . Этот делегат соответствует любым методам, которые имеют тип void и принимают параметр типа string .
В классе Account определяем переменную taken , которая представляет этот делегат:
- Теперь надо связать эту переменную с конкретным действием, которое будет выполняться. Мы можем использовать разные способы для передачи делегата в класс. В данном случае определяется специальный метод RegisterHandler, в котором в переменную taken передается реальное действие:
- Таким образом, делегат установлен, и теперь его можно вызывать. Вызов делегата производится в методе Take:
Поскольку делегат AccountHandler в качестве параметра принимает строку, то при вызове переменной taken() мы можем передать в этот вызов конкретное сообщение. В зависимости от того, произошло снятие денег или нет, в вызов делегата передаются разные сообщения.
- То есть фактически вместо делегата будут выполняться действия, которые переданы делегату в методе RegisterHandler. Причем опять же, при вызове делегата мы не знаем, что это будут за действия. Здесь мы только передаем в эти действия сообщение об успешно или неудачном снятии.
- Теперь протестируем класс в основной программе:
Здесь через метод RegisterHandler переменной taken в классе Account передается ссылка на
метод PrintSimpleMessage . Этот метод соответствует делегату AccountHandler. Соответственно там, где в классе Account вызывается делегат taken, в реальности будет выполняться метод PrintSimpleMessage.
Через параметр message метод PrintSimpleMessage получит переданное из делегата сообщение и
выведет его на консоль:
- Таким образом, мы создали механизм обратного вызова для класса Account, который срабатывает в случае снятия денег. Здесь мы выводим сообщение на консоль. Мы могли бы просто выводить сообщение на консоль и без делегатов. Однако с делегатом для класса Account не важно, как это сообщение выводится. Классу Account даже не известно, что вообще будет делаться в результате списания денег. Он просто посылает уведомление об этом через делегат.
- В результате, если мы создаем консольное приложение, мы можем через делегат выводить сообщение на консоль. Если мы создаем графическое приложение Windows Forms или WPF, то можно выводить сообщение в виде графического окна. А можно не просто выводить сообщение. А, например, записать при списании информацию об этом действии в файл или отправить уведомление на электронную почту. В общем любыми способами обработать вызов делегата. И способ обработки не будет зависеть от класса Account.
Добавление и удаление методов в делегате
- Хотя в примере наш делегат принимал адрес на один метод, в действительности он может указывать сразу на несколько методов. Кроме того, при необходимости мы можем удалить ссылки на адреса определенных методов, чтобы они не вызывались при вызове делегата. Итак, изменим в классе Account метод RegisterHandler и добавим новый метод UnregisterHandler, который будет удалять методы из списка методов делегата:
В первом методе объединяет делегаты taken и del в один, который потом присваивается переменной taken .
Во втором методе из переменной taken удаляется делегат del .
Применим новые методы:
- В целях тестирования мы создали еще один метод - PrintColorMessage, который выводит то же самое сообщение только красным цветом. Ссылка на этот метод также передается в метод RegisterHandler, и таким образом ее получит переменная taken.
В строке account.UnregisterHandler(PrintColorMessage); этот метод удаляется из списка вызовов делегата, поэтому этот метод больше не будет срабатывать. Консольный вывод будет иметь следующую
форму:
Анонимные методы
- С делегатами тесно связаны анонимные методы . Анонимные методы используются для создания экземпляров делегатов.
- Определение анонимных методов начинается с ключевого слова delegate , после которого идет в скобках список параметров и тело метода в фигурных скобках:
- Анонимный метод не может существовать сам по себе, он используется для инициализации экземпляра делегата , как в данном случае переменная handler представляет анонимный метод. И через эту переменную делегата можно вызвать данный анонимный метод.
- Другой пример анонимных методов - передача в качестве аргумента для параметра, который представляет делегат:
- Если анонимный метод использует параметры, то они должны соответствовать параметрам делегата. Если для анонимного метода не требуется параметров, то скобки с параметрами опускаются. При этом даже если делегат принимает несколько параметров, то в анонимном методе можно вовсе опустить параметры:
- То есть если анонимный метод содержит параметры, они обязательно должны соответствовать параметрам делегата. Либо анонимный метод вообще может не содержать никаких параметров, тогда он соответствует любому делегату, который имеет тот же тип возвращаемого значения.
- При этом параметры анонимного метода не могут быть опущены, если один или несколько параметров определены с модификатором out .
- Также, как и обычные методы, анонимные могут возвращать результат:
- При этом анонимный метод имеет доступ ко всем переменным, определенным во внешнем коде:
- В каких ситуациях используются анонимные методы? Когда нам надо определить однократное действие, которое не имеет много инструкций и нигде больше не используется. В частности, их можно использовать для обработки событий.