Делегаты, лямбды и события в C#: просто о сложном
Сегодня поговорим о делегатах. Но речь пойдет не о болтливых представителях какой-нибудь партии на очередном съезде, а об объектах-указателях, используемых в программировании, в частности в языке C#. Делегаты содержат в себе ссылки на несколько методов, которые вызываются по мере необходимости.
Иначе говоря, если создать определенное количество методов и прикрепить их к делегатам (c# delegate
), прописанным в нашей программе, то во время выполнения этой программы они динамически вызовут нужный нам метод.
Содержание:
1. Что такое делегаты на простом примере
2. Что надо знать о делегатах
3. Примеры применения делегатов
4. Multicast-делегаты
5. Классы System.Delegate и System.MulticastDelegate
6. Цепочка делегатов
7. Методы и свойства делегатов
8. Лямбда-выражения в C#
9. Обработка событий
10. Обобщающий пример
11. Заключение
Что такое делегаты (c# delegate) на примере
Для того, чтобы лучше понять работу с делегатами (c# delegate event
), давайте рассмотрим небольшой пример из реальной жизни.
Представьте, что, находясь в аэропорту, вы вдруг услышали сигнал пожарной сигнализации. Какие будут ваши действия? Конечно же бежать, не жалея ног, к ближайшему аварийному выходу. Но ситуации в жизни бывают разные, в идеале необходимо заранее предусмотреть все варианты развития событий (что характерно для любого хорошего программиста). Поэтому давайте далее предположим, что еще может произойти в этом сценарии:
- Когда вы слышите пожарную тревогу (это событие), вы начинаете немедленно действовать, а именно — бежать к выходу
run()
. - Если, вдруг вы услышали перед сигнализацией объявление о посадке на свой самолет, вы возьмете посадочный талон и быстро направитесь ко входу на свой рейс, ведь билеты уже куплены, а вы целый год мечтали полететь на море.
- Если по громкой связи объявят о том, что вас ищет мама, приехавшая вас проводить, вы конечно сначала пойдете к столу справок, чтобы покинуть зал вместе.
- Но если после сигнализации диктор объявит, что это была обычная учебная тревога, то тогда вы останетесь в зале аэропорта, чтобы дожидаться положенного времени отправления (отменяя все первоначальные планы).
Из примера видно, что мы по-разному реагируем на происходящее в зависимости от хода развития события. В программировании эту вариативность реакций привносит делегат — он отслеживает события Event
и реагирует на изменение исходной ситуации, запуская созданные заранее пронумерованные методы.
Что надо знать о делегатах
- У них ссылочный тип, но они ссылаются не на объекты, а на методы, выполняя роль указателя;
- По одному событию может быть вызвано сразу несколько методов;
- Тип делегата устанавливается по его имени;
- Отсутствует тело метода;
- Они по умолчанию объектно-ориентированы и типабезопасны;
- Инкапсулируют необходимые методы в наш код;
- Функция (метод), добавляемая к делегатам, должна иметь тот же тип и сигнатуру, что и делегат;
Cоздать делегат в C# просто — достаточно написать перед методом ключевое слово delegate
.
Они бывают:
- без параметра и без типа возвращаемого значения:
private delegate ExampleDelegate();
- с параметром, но без типа:
private delegate ExampleDelegate(object item1, object item1);
или
private delegate ExampleDelegate(String text);
- с параметром и типом:
private delegate int ExampleDelegate(object item1, object item2);
Примеры применения делегатов
Рассмотрим принцип работы делегатов на примере:
Using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp{ public class ExampleDelegate{ //Объявление //Создаем делегат без параметров и возвращаемого типа public delegate void FstDelegate(); public void func1(){ Console.WriteLine("Function1"); } public void func2(){ Console.WriteLine("Function2"); } public void func3(){ Console.WriteLine(" Function3"); } } class Prog{ static void Main(string[] args){ ExampleDelegate exdelegate = new ExampleDelegate(); //прикрепляем делегату методы ExampleDelegate.FstDelegate fd1 = new ExampleDelegate.FstDelegate(exdelegate.func1); ExampleDelegate.FstDelegate fd2 = new ExampleDelegate.FstDelegate(exdelegate.func2); ExampleDelegate.FstDelegate fd3 = new ExampleDelegate.FstDelegate(exdelegate.func3); //Вызываем методы через делегат fd1(); fd2(); fd3(); Console.ReadKey(); } } }
Выведет:
Function1 Function2 Function3
Еще один полезный пример:
namespace DelegateExample{ class Prog{ public delegate int calculate(int x, int y); static int Add(int a, int b){ return a + b; } static int Sub(int a, int b){ return a - b; } static void Main(string[] args){ calculate c = new calculate(Prog.Add); Console.WriteLine("Сумма 5 и 10 : {0}", c(5, 10)); calculate d = new calculate(Prog.Sub); Console.WriteLine("Разность 5 и 10 : {0}", d(5, 10)); Console.ReadKey(); } } }
Выведет:
Сумма 5 и 10 : 15 Разность 5 и 10 : -5
Multicast-делегаты
Многоадресными (Multicast
) называются делегаты, сочетащие в себе несколько методов одновременно. Что надо знать о них:
- При таком виде делегатов вызывается сразу несколько методов;
- Все методы вызываются по принципу
FIFO
(First in First Out), то есть первым написан — первым сработает; - Операторы
+
и+ =
используем, чтобы добавить очередной метод к делегату; - Операторы
-
и- =
используем, чтобы удалить лишний, уже ненужный метод из списка.
Посмотрим как это работает:
namespace MulticastDelegates { class ExDelegate { public delegate void ShowMsg(string s); //создали делегат public void msg1(string msg) // создаем 3 метода { Console.WriteLine("1stMessage is : {0}", msg); } public void msg2(string msg) { Console.WriteLine("2ndMessage is : {0}", msg); } public void msg3(string msg) { Console.WriteLine("3rdMessage is : {0}", msg); } } class Prog { static void Main(string[] args) { ExDelegate td = new ExDelegate(); ExDelegate.ShowMsg message = null; //Создан объект делегата в методе Main message += new ExDelegate.ShowMsg(td.msg1); message += new ExDelegate.ShowMsg(td.msg2); message += new ExDelegate.ShowMsg(td.msg3); //Все методы добавляются к объекту делегата. message("MulticastDelegates"); // выводим в консоль сообщение message -= new ExDelegate.ShowMsg(td.msg2); // Удаляем метод msg2(string msg), открепляя его от делегата Console.WriteLine("----------------------"); message("Message2Removed"); Console.ReadKey(); } } }
Выведет:
1stMessage is : MulticastDelegates 2ndMessage is : MulticastDelegates 3rdMessage is : MulticastDelegates ---------------------- 1stMessage is : Message2Removed 3rdMessage is : Message2Removed
Классы System.Delegate и System.MulticastDelegate
Delegate и MulticastDelegate — базовые классы для всех типов делегатов. В C# при написании ключевого слова delegate
и создании делегата компилятору языка предоставляется возможность создать класс, производный от двух рассматриваемых нами классов. Но лишь система и компилятор явно наследуются от них. Также у нас нет возможности создания своего типа, наследуемого от этих классов.
Другими словами, любой делегат — это класс унаследованный от базового класса System.Delegate
и System.MulticastDelegate
.
Цепочка делегатов
Экземпляр делегата может быть оболочкой для нескольких методов. Это достаточно удобная возможность для разработчика при реализации обработки событий. При таком варианте применения это сопровождается запуском серии методов при появлении всего лишь одного UI-события (нажатие кнопок, клика мыши и др.). Как мы уже говорили ранее, делегат, содержащий в себе ссылки на несколько методов, называют многоадресным, а последовательный вызов методов по ссылке, при одиночном обращении к делегату — цепочкой вызовов делегата.
Методы и свойства делегатов
В таблице ниже приведены основные методы и свойства делегатов в C#:
Лямбда-выражения в C#
По сути, лямбда-выражение — это структура, не имеющая объявления, без модификатора доступа или имени, и используемая для создания емких лаконичных методов. При этом возвращающая некое значение, далее передавая его в качестве параметров в другие методы. Чтобы разобраться с такой непростой формулировкой, необходимо понять синтаксис лямбда-выражений в C#.
(Input parameters) => Expression or statement block
Код можно разделить на две части — левую и правую. Левая часть используется для ввода списка параметров, а правая часть — для написания собственно самих выражений.
Пример:
class Prog { delegate int Operator(int a, int b); static void Main(string[] args) { Operator operator = (a, b) => a + b; Console.WriteLine(operator(5, 7)); // 12 Console.WriteLine(operator(11, 33)); // 44 Console.Read(); } }
Конструкция (a, b) => a + b
— лямбда-выражение, где в левой части обозначены параметры a, b
, а в правой — непосредственно само выражение a + b
. Обратите внимание, что при таком объявлении не нужно указывать тип параметра и нет необходимости в операторе return
.
Еще одна особенность данной структуры — неявное преобразование каждого параметра лямбда-выражения в соответствующий параметр делегата. Это значит, что тип у них должен быть один (одинаковый), а число параметров должно равняться числу параметров делегата. Кроме того, должны быть равны их возвращаемые значения.
Лямбда-выражения могут быть без параметров:
() => Console.WriteLine("This is a lambda expression without any parameter"); иметь один или несколько параметров: (a, numbers) => a.quantity>= numbers с указанным типом параметра: (a, int numbers) => a.quantity>= numbers; с несколькими операторами в фигурных скобках: (a, 10) => { Console.WriteLine("This is an example of a lambda expression with multiple statements"); return a.quantity >= 10; }
Обработка событий
Под событием понимают любые действия пользователя: нажатие клавиш, клики мышкой, скроллинг и т.п. Они нужны для удобства работы с пользовательским интерфейсом, программными уведомлениями и обеспечивают связь между различными классами. Как мы уже говорили, возникшее событие отправляет сигнал делегатам, которые, в свою очередь, выполняют нужную функцию (при помощи обработчика событий).
Обработчик событий — это не что иное, как метод, вызываемый с помощью делегата. Чтобы связать событие с уже существующим экземпляром делегата, нужно использовать оператор + =
:
obj.MyEvent + = new MyDelegate (obj.Display);
В общем смысле, событие — это метод, используемый для запуска необходимой функции. В отличие от делегата событие обрабатывается как член экземпляра. И в этом принципиальная разница между ними.
В основе реализации концепции событий лежит шаблон проектирования Publish-Subscribe
(издатель-подписчик), при котором объект-издатель оповещает всех своих подписчиков, путем вызова у них какого-то метода.
Событие имеет синтаксис:
public event <название_делегата> <Название_события>;
Название делегата — это его имя, на которое «ссылаются» методы.
Обобщающий пример
Но хватит теории, давайте решим небольшую задачу.
Допустим, мы хотим, чтобы при нажатии кнопки на телефоне или при повышении температуры до определенного уровня происходило следующее:
- включался кондиционер
- закрывались двери и окна
- приехал грузовик с мороженым
Эти действия (методы), которые будут делегированы объектом-делегатом при возникновении события (вручную при нажатии кнопки или автоматически при повышении температуры).
Итак, у нас есть два триггера событий: мобильный телефон и термометр, мы можем использовать интерфейс, потому что оба они будут иметь одинаковый код:
public interface IEventTrigger{ event EventHandler<TemperatureEventArgs> OnTemperatureChanged; int Temperature { set; } }
Обратите внимание, я использую общий делегат EventHandler <>
, передавая свой собственный пользовательский класс EventArgs
:
public class TemperatureEventArgs : EventArgs{ public int Value { get; set; } }
TemperatureEventArgs
унаследовал класс EventArgs
и будет содержать только одну часть необходимой информации – значение температуры.
Создадим класс издателя, который будет реализовывать событие IEventTrigger
:
public class Thermometer : IEventTrigger{ private int maxTemperature; public Thermometer(int maxTemperature){ this.maxTemperature = maxTemperature; } public event EventHandler<TemperatureEventArgs> OnTemperatureChanged = delegate{}; public int Temperature{ set{ var temperature = value; if(temperature > maxTemperature){ var temperatureValue = new TemperatureEventArgs{ Value = temperature, }; OnTemperatureChanged(this, temperatureValue); } } } }
Класс телефона — это еще один триггер, и он будет иметь тот же код, что и класс термометра.
Класс термометра и класс телефона имеют частное поле, в котором будет храниться максимальное значение температуры, установленное с помощью конструктора.
Каждый раз, когда температура будет подниматься выше максимального значения (определенного нами уровня), термометр будет вызывать делегата:
OnTemperatureChanged (this, temperatureValue);
Он и позаботится о выполнении действий подписчика.
Как подписчик узнает, когда его методы будут выполнены? Издатель не только сообщает подписчикам, когда, но и запускает выполнение методов подписчика через делегат OnTemperaChanged
.
Ключевое слово this
относится к элементу управления (триггеру), в данном случае это термометр. Параметр temperatureValue
представляет собой текущее значение температуры (которое измеряется термометром) и передается через аргумент события подписчику (приложению или устройству, которое будет выполнять действия или выполнять методы, удерживаемые делегатом).
А вот подписчик, отслеживающий триггеры для выполнения некоторых действий:
static void Main(string[] args){ IEventTrigger trigger = new Thermometer(30);// или IEventTrigger trigger = new Mobile(30); trigger.OnTemperatureChanged += (o, e) => Console.WriteLine($"Temperature is changed to {e.Value} °C. {((o is Mobile) ? "Triggered manually by mobile." : "Triggered automatically by the thermometer.")}"); trigger.OnTemperatureChanged += (o, e) => Console.WriteLine("1. Приезжает грузовик с мороженым!"); trigger.OnTemperatureChanged += (o, e) => Console.WriteLine("2. Включается кондиционер."); trigger.OnTemperatureChanged += (o, e) => Console.WriteLine("3. Закрываются все окна и двери."); trigger.Temperature = 32; //при температуре ниже 30 никаких действий выполняться не будет. }
Как только температура будет изменена (удаление издателя и подписчика), будет запущен установленный аксессор триггера . Метод доступа set
проверяет, не превышает ли текущая температура наивысший температурный уровень. Если он выше, то делегат OnTemperaChanged
выполняется в объекте издателя (напомним, триггер: термометр или мобильный телефон).
В этом примере используется только один подписчик, но у вас их может быть несколько. Подписчик использует знак + =
, чтобы подписаться на события издателя. Он как бы просит, сообщает издателю: «Пожалуйста, выполните эти действия, когда будет выполнено какое-то указанное условие». Условие определяет издатель, а действия — подписчики.
Заключение
Подводя итог всему написанном стоит отметить, что использование делегатов сделает ваш код более элегантным и компактным. Такой подход позволит вам избавиться от написания вереницы никому не нужных циклов, за счет передачи готовых кусков исполняемого кода в качестве аргументов в нужные нам функции.
По традиции несколько полезных ссылок на закрепляющие видео по этой теме:
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: