В прошлой теме подробно были рассмотрены делегаты. Однако данные примеры, возможно, не показывают истинной силы делегатов, так как нужные нам методы в данном случае мы можем вызвать и напрямую без всяких делегатов. Однако наиболее сильная сторона делегатов состоит в том, что они позволяют делегировать выполнение некоторому коду извне. И на момент написания программы мы можем не знать, что за код будет выполняться. Мы просто вызываем делегат. А какой метод будет непосредственно выполняться при вызове делегата, будет решаться потом.
Рассмотрим подробный пример. Пусть у нас есть класс, описывающий счет в банке:
public class Account { int sum; // Переменная для хранения суммы // через конструктор устанавливается начальная сумма на счете public Account(int sum) => this.sum = sum; // добавить средства на счет public void Add(int sum) => this.sum += sum; // взять деньги с счета public void Take(int sum) { // берем деньги, если на счете достаточно средств if (this.sum >=sum) this.sum -= sum; } }
В переменной sum хранится сумма на счете. С помощью конструктора устанавливается начальная сумма на счете. Метод Add()
служит для добавления на счет,
а метод Take
- для снятия денег со счета.
Допустим, в случае вывода денег с помощью метода Take
нам надо как-то уведомлять об этом самого владельца счета и, может
быть, другие объекты. Если речь идет о консольной программе, и класс будет применяться в том же проекте, где он создан, то мы можем написать просто:
public class Account { int sum; public Account(int sum) => this.sum = sum; public void Add(int sum) => this.sum += sum; public void Take(int sum) { if (this.sum >= sum) { this.sum -= sum; Console.WriteLine($"Со счета списано {sum} у.е."); } } }
Но что если наш класс планируется использовать в других проектах, например, в графическом приложении на Windows Forms или WPF, в мобильном приложении, в веб-приложении. Там строка уведомления
Console.WriteLine($"Со счета списано {sum} у.е.");
не будет иметь большого смысла.
Более того, наш класс Account будет использоваться другими разработчиками в виде отдельной библиотеки классов. И эти разработчики захотят уведомлять о снятии средств каким-то другим образом, о которых мы даже можем не догадываться на момент написания класса. Поэтому примитивое уведомление в виде строки кода
Console.WriteLine($"Со счета списано {sum} у.е.");
не самое лучшее решение в данном случае. И делегаты позволяют делегировать определение действия из класса во внешний код, который будет использовать этот класс.
Изменим класс, применив делегаты:
// Объявляем делегат public delegate void AccountHandler(string message); public class Account { int sum; // Создаем переменную делегата AccountHandler? taken; public Account(int sum) => this.sum = sum; // Регистрируем делегат public void RegisterHandler(AccountHandler del) { taken = del; } public void Add(int sum) => this.sum += sum; public void Take(int sum) { if (this.sum >= sum) { this.sum -= sum; // вызываем делегат, передавая ему сообщение taken?.Invoke($"Со счета списано {sum} у.е."); } else { taken?.Invoke($"Недостаточно средств. Баланс: {this.sum} у.е."); } } }
Для делегирования действия здесь определен делегат AccountHandler. Этот делегат соответствует любым методам, которые имеют тип void и принимают параметр типа string.
public delegate void AccountHandler(string message);
В классе Account определяем переменную taken
, которая представляет этот делегат:
AccountHandler? taken;
Теперь надо связать эту переменную с конкретным действием, которое будет выполняться. Мы можем использовать разные способы для передачи делегата в класс. В данном случае определяется специальный метод RegisterHandler, в котором в переменную taken передается реальное действие:
public void RegisterHandler(AccountHandler del) { taken = del; }
Таким образом, делегат установлен, и теперь его можно вызывать. Вызов делегата производится в методе Take:
public void Take(int sum) { if (this.sum >= sum) { this.sum -= sum; // вызываем делегат, передавая ему сообщение taken?.Invoke($"Со счета списано {sum} у.е."); } else { taken?.Invoke($"Недостаточно средств. Баланс: {this.sum} у.е."); } }
Поскольку делегат AccountHandler в качестве параметра принимает строку, то при вызове переменной taken()
мы можем передать в этот вызов
конкретное сообщение. В зависимости от того, произошло снятие денег или нет, в вызов делегата передаются разные сообщения.
То есть фактически вместо делегата будут выполняться действия, которые переданы делегату в методе RegisterHandler. Причем опять же подчеркну, при вызове делегата мы не знаем, что это будут за действия. Здесь мы только передаем в эти действия сообщение об успешно или неудачном снятии.
Теперь протестируем класс в основной программе:
// создаем банковский счет Account account = new Account(200); // Добавляем в делегат ссылку на метод PrintSimpleMessage account.RegisterHandler(PrintSimpleMessage); // Два раза подряд пытаемся снять деньги account.Take(100); account.Take(150); void PrintSimpleMessage(string message) => Console.WriteLine(message);
Здесь через метод RegisterHandler переменной taken в классе Account передается ссылка на метод PrintSimpleMessage
. Этот метод
соответствует делегату AccountHandler. Соответственно там, где в классе Account вызывается делегат taken, в реальности будет выполняться метод
PrintSimpleMessage.
Через параметр message
метод PrintSimpleMessage получит переданное из делегата сообщение и выведет его на консоль:
Со счета списано 100 у.е. Недостаточно средств. Баланс: 100 у.е.
Таким образом, мы создали механизм обратного вызова для класса Account, который срабатывает в случае снятия денег. Здесь мы выводим сообщение на консоль. Да, мы могли бы просто выводить сообщение на консоль и без делегатов. Однако с делегатом для класса Account не важно, как это сообщение выводится. Классу Account даже не известно, что вообще будет делаться в результате списания денег. Он просто посылает уведомление об этом через делегат.
В результате, если мы создаем консольное приложение, мы можем через делегат выводить сообщение на консоль. Если мы создаем графическое приложение Windows Forms или WPF, то можно выводить сообщение в виде графического окна. А можно не просто выводить сообщение. А, например, записать при списании информацию об этом действии в файл или отправить уведомление на электронную почту. В общем любыми способами обработать вызов делегата. И способ обработки не будет зависеть от класса Account.
Хотя в примере наш делегат принимал адрес на один метод, в действительности он может указывать сразу на несколько методов. Кроме того, при необходимости мы можем удалить ссылки на адреса определенных методов, чтобы они не вызывались при вызове делегата. Итак, изменим в классе Account метод RegisterHandler и добавим новый метод UnregisterHandler, который будет удалять методы из списка методов делегата:
public delegate void AccountHandler(string message); public class Account { int sum; AccountHandler? taken; public Account(int sum) => this.sum = sum; // Регистрируем делегат public void RegisterHandler(AccountHandler del) { taken += del; } // Отмена регистрации делегата public void UnregisterHandler(AccountHandler del) { taken -= del; // удаляем делегат } public void Add(int sum) => this.sum += sum; public void Take(int sum) { if (this.sum >= sum) { this.sum -= sum; taken?.Invoke($"Со счета списано {sum} у.е."); } else taken?.Invoke($"Недостаточно средств. Баланс: {this.sum} у.е."); } }
В первом методе объединяет делегаты taken
и del
в один, который потом
присваивается переменной taken
. Во втором методе из переменной taken
удаляется делегат del
.
Применим новые методы:
Account account = new Account(200); // Добавляем в делегат ссылку на методы account.RegisterHandler(PrintSimpleMessage); account.RegisterHandler(PrintColorMessage); // Два раза подряд пытаемся снять деньги account.Take(100); account.Take(150); // Удаляем делегат account.UnregisterHandler(PrintColorMessage); // снова пытаемся снять деньги account.Take(50); void PrintSimpleMessage(string message) => Console.WriteLine(message); void PrintColorMessage(string message) { // Устанавливаем красный цвет символов Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(message); // Сбрасываем настройки цвета Console.ResetColor(); }
В целях тестирования мы создали еще один метод - PrintColorMessage, который выводит то же самое сообщение только красным цветом. Ссылка на этот метод также передается в метод RegisterHandler, и таким образом ее получит переменная taken.
В строке account.UnregisterHandler(PrintColorMessage);
этот метод удаляется из списка вызовов делегата, поэтому этот метод
больше не будет срабатывать. Консольный вывод будет иметь следующую форму:
Со счета списано 100 у.е. Со счета списано 100 у.е. Недостаточно средств. Баланс: 100 у.е. Недостаточно средств. Баланс: 100 у.е. Со счета списано 50 у.е.