Від плутанини до ясності: делегати пояснені правильно

Кожна стаття, яку я бачив, що говорить про делегати, завжди починається з питання "що таке делегати?" і продовжується чимось на кшталт: "це спосіб передавати методи методам". Хоча це вірно, насправді важко зрозуміти, про що йдеться. Звідки вони з'являються?

Якщо ви маєте таке відчуття, ви не самотні. Ось чому я пишу цю статтю. Почнемо!

Я хочу почати з змінних. Так, ви правильно прочитали — змінних!

Ми всі знаємо, що змінні можуть бути або типами за значенням, або типами за посиланням. Нас цікавлять змінні типів за посиланням. Є два варіанти змінних типів за посиланням:

• Вбудовані змінні

• Користувацькі змінні

Прикладами вбудованих змінних є такі, як string, це псевдонім для класу System.String.

Прикладами користувацьких типів є класи, які ми використовуємо для створення об'єктів, що містять користувацькі дані та методи.

Загальна риса між цими двома типами — це спосіб їх зберігання. Кожна змінна містить посилання/місцезнаходження об'єкта, який зберігається в пам'яті.

Ми звикли бачити щось на кшталт цього:

• Person x = new Person();

X — це змінна, яка утримує місцезнаходження об'єкта person в пам'яті.

Ми дуже комфортно почуваємось із такими об'єктами, але що якщо я скажу, що ми можемо використовувати змінні посилання для зберігання методів? 🙂

Ви можете почуватися незручно і задати ці два питання:

• Як? Ми будемо використовувати користувацькі типи чи вбудовані типи?

• Чому?

Відповідь на друге питання — це одне слово, до якого ми повернемося після того, як дуже чітко відповімо на перше питання.

Це Callback (зворотний виклик)!

Тепер давайте відповімо на питання "Як?"

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

Щоб використати це ключове слово, треба зробити два кроки:

  1. Оголосити підпис методу, який ми хочемо, щоб змінна делегата утримувала.

  2. Призначити реальний метод для цієї змінної делегата. Цей реальний метод може бути одним з 3-х варіантів:

• Іменований метод

• Анонімний метод

• Лямбда-вираз

Візьмемо приклад. Припустимо, у нас є такий метод:

public static void PrintToConsole(string message)  
{  
 Console.WriteLine("Console: " + message);  
}

Що ми хочемо — це змінна, яка зможе утримувати методи, тому давайте скористаємося нашими 2-ма кроками:

  1. Який підпис методу ми хочемо утримувати? Ну, це PrintToConsole. Зачекайте, я сказав підпис, а не ім'я! 😞

Отже, це метод, який приймає один параметр і нічого не повертає.

Тож давайте оголосимо нашу змінну:

(delegatekeyword) variablename (string x)

Отже, ми маємо:

delegate void PrintToConsoleDelegate(string x);
  1. Тепер давайте призначимо ім'я методу:
PrintToConsoleDelegate printDelegate = PrintToConsole;

Після визначення делегата, що ми можемо з ним зробити? Багато чого, але поки що зробимо одне: викличемо делегат.

printDelegate("Hello, this is a delegate invoke");

Давайте з'єднаємо все, щоб побачити повну картину:

class Program  
{  
 delegate void PrintToConsoleDelegate(string x);  

 public static void Main()  
 {  
 PrintToConsoleDelegate printDelegate = PrintToConsole;  
 printDelegate("Hello, this is a delegate invoke");  
 }  

 static void PrintToConsole(string message)  
 {  
 Console.WriteLine(message);  
 }  
}

Запустіть проєкт, і ви побачите чорне вікно консолі з таким текстом: "Hello, this is a delegate invoke"

Ми вже згадували, що на другому етапі треба призначити метод, і ми згадали 3 способи призначення методу. Ми побачили перший спосіб, який був іменованим методом. Тепер давайте подивимося на другий спосіб: Анонімний метод.

Перед тим, як створити його, ідея дуже проста. Якщо ви не хочете створювати конкретний метод для вашого делегата і хочете використовувати метод лише для делегатів, створіть анонімний.

Давайте перерахуємо попередній приклад ще раз, не визначаючи жодного методу явно.
Давайте дотримуватись наших 2 кроків:

delegate void PrintToConsoleDelegate(string x);
PrintToConsoleDelegate printDelegate = delegate(string message)   
{  
 Console.WriteLine("Console: " + message);  
};

Спосіб виклику тут такий самий, як і в попередньому прикладі.

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

delegate void PrintToConsoleDelegate(string x);
PrintToConsoleDelegate printDelegate = (string x) => Console.WriteLine("Console: " + x);

Вітаємо! Тепер ми знаємо, що таке делегати, як їх створювати та як призначати методи для них трьома способами.

Можна зупинитись на цьому, якщо ви хочете перевести дух. Наступне поняття — це Попередньо визначені делегати.

До цього часу ми створювали власні делегати, але уявіть, як би це було важко, якби в нашому коді було 100 методів, які потрібно було б призначити до змінних делегатів. Нам довелося б створювати 100 змінних делегатів, що є абсурдним і робить код схожим на локшину.

Тому Microsoft допомогла нам з попередньо визначеними делегатами. Що це означає? Пам'ятаєте наші 2 кроки? Здається, що перший крок нам вже не потрібен (так!). Нам потрібен тільки другий крок.

Який захват! Де ці попередньо визначені делегати? Скільки їх є? Як вони охоплюють всі підписи методів?

Ну, є 3 попередньо визначених делегата:

Action: Використовуйте його, коли у вас є метод в коді, який приймає один параметр (неважливо його тип) і не повертає значення.

Func: Використовуйте його, коли у вас є метод в коді, який приймає один параметр (до 16) і повертає результат.

Predicate: Використовуйте його, коли у вас є метод в коді, який приймає один параметр і повертає логічне значення.

Можливо, ви запитаєте, чому у нас є Predicate — чи не те ж саме, що і Func?

Так, це так, але чому б і ні? 😉

Давайте знову подивимося на наш приклад. Наш підпис той самий: це метод, який приймає один параметр і нічого не повертає. Тож який з попередньо визначених типів делегатів підходить? Це Action.

Отже, наш код оновлюється так:

Action printDelegate = (string x) => Console.WriteLine("Console: " + x);

Хіба це не простіше? Використовувати попередньо визначені делегати з лямбда-виразами дійсно весело, і саме так побудований LINQ. Ми до цього ще дійдемо. 🙂

Давайте визначимо ще один метод, щоб концепція попередньо визначених делегатів краще запам'яталася.

Припустимо, у нас є цей метод:

public int Add(int x, int y)  
{  
 return x + y;  
}

Якщо ми хочемо призначити його до змінної делегата, це буде Func.

Отже, це виглядає ось так:

Func<int, int, int> addDelegate = (int x, int y) => x + y;

Тепер ми пройшли довгий шлях, і ви, мабуть, думаєте, що я забув про друге питання: навіщо вони нам потрібні? Сподіваюся, ви пам'ятаєте мою коротку відповідь: Callback (зворотний виклик).

По-перше, що таке callback? Це частина виконуваного коду (метод), яка передається як аргумент іншому коду (іншому методу). Інший код очікує, що виконає цей шматок коду в зручний час.

Все це через callback? 😞 Чому вони важливі?

Насправді, вони надзвичайно важливі! Callbacks дозволяють нам підключати новий код до існуючого коду.

Тому ми можемо інжектувати поведінку (метод), яку нам потрібно, у будь-яку існуючу частину коду, що робить код більш гнучким (динамічний виклик, різні стратегії під час виконання) і застосовує принцип відкритості/закритості. Існуючий код не відкритий для редагування; ми можемо додавати будь-який новий код, не торкаючись старого.

Давайте подивимося на приклад. Припустимо, у нас є цей метод:

public void ProcessData(string data)  
{  
 var processedData = data.ToUpper();  
}

У нас є цей метод, і наш клієнт, який хоче використовувати цей метод, хоче мати можливість вивести оброблені дані на консоль чи в файл.
Один зі способів, який ми могли б використати, це додати булевий прапорець у підпис методу, який вказує, як саме ми виводимо дані.

Отже, ми отримаємо щось на кшталт цього:

public void ProcessData(string data, bool isFile)  
{  
 var processedData = data.ToUpper();  
 if (isFile)  
 Console.WriteLine("File: " + processedData);  
 else  
 Console.WriteLine("Console: " + processedData);  
}

Згодом клієнт прийшов і сказав, що вони хочуть додати ще одну стратегію виведення. Тепер нам потрібно оновити метод і додати ще один додатковий блок, що робить наш код більш вразливим і може призвести до помилок, якщо ми випадково пропустимо одну з умов. Крім того, якщо у вас є SonarQube чи будь-який інший інструмент для аналізу складності коду, він вимагатиме спростити ці блоки (це називається ‘Cyclomatic complexity’).

Давайте подумаємо інакше. Ми хочемо передавати різні стратегії виведення під час виконання і залишити контроль у руках клієнта.

Отже, клієнт хоче, щоб ми обробляли різні стратегії виведення в залежності від поведінки, яку вони хочуть зараз чи можуть захотіти пізніше. Коли ви чуєте "стратегія під час виконання", подумайте про делегати, навіть якщо це не завжди так. Спробуйте!

Тепер питання: якщо ми думаємо про делегати, як виглядатиме ця поведінка? Нам потрібно передати оброблені дані до методу, який нічого не повертає. Я бачу, що це те саме, що і делегат Action.

Давайте подивимося, як ми це реалізуємо:

public void ProcessData(string data, Action printStrategy)  
{  
 var processedData = data.ToUpper();  
 printStrategy(processedData);  
}

Як ми це викликаємо? Клієнт напише код такого вигляду:

ProcessData("Delegates article", x => Console.WriteLine("Console: " + x));

Наскільки це зручно і гнучко? 🙂

Отже, тепер у нас є розуміння того, як використовувати делегати як зворотні виклики (callbacks), але чому ж не можна просто писати спагеті-код і забути про все це?

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

Маю впевненість, що ви бачили щось на кшталт цього:

var list = list.Where(x => x == 5).ToList();
var orders = collection.Select(x => new Order { }).ToList();
users.ForEach(x => x.UpdatedDate = DateTime.Now);

Це операції LINQ, які побудовані на основі делегатів і лямбда-виразів. Чому делегати? Тому що саме так ми можемо передавати необхідну поведінку всередину їхніх методів.

Делегати зустрічаються в різних ситуаціях і численних прикладах, але я вірю, що якщо ви зрозумієте основну ідею цього механізму, ви зрозумієте все, що побудовано на його основі.

Дякую за увагу!

Перекладено з: From Confusion to Clarity: Delegates Explained Right

Leave a Reply

Your email address will not be published. Required fields are marked *