Уявіть, що ви створюєте систему обробки файлів. Коли програма знаходить файл, вона має повідомити різні частини вашого додатку: один компонент оновлює UI, інший логує результат, третій відправляє статистику. Як зробити це елегантно, не створюючи жорстких зв'язків між класами?
Делегати (Delegates) вирішують цю проблему. Вони є типобезпечними покажчиками на методи, які дозволяють передавати методи як параметри, зберігати їх у колекціях та викликати динамічно.
У мові C існували вказівники на функції — потужний, але небезпечний механізм. C# взяв цю ідею та зробив її типобезпечною:
Перед вивченням цієї теми вам потрібно розуміти:
Делегат (Delegate) — це тип, що представляє посилання на методи з певною сигнатурою. По суті, це клас, похідний від System.MulticastDelegate, який описує контракт методу.
На низькому рівні делегат — це об'єкт, що містить посилання на метод та об'єкт, до якого цей метод належить.
_target: посилання на об'єкт (для instance методів) або null (для static методів)._methodPtr: вказівник на функцію в пам'яті.Коли ви створюєте MulticastDelegate, він додатково містить список інших делегатів, які потрібно викликати послідовно.
Теоретичні аспекти реалізації делегатів:
Делегати реалізовані як класи, похідні від System.Delegate, який сам є похідним від System.Object. Кожен делегат містить внутрішню структуру, що включає:
Коли ви викликаєте делегат, CLR (Common Language Runtime) виконує наступні кроки:
Ця реалізація забезпечує типобезпечність та ефективність викликів методів через делегати.
Оголошення делегата схоже на оголошення методу, але з ключовим словом delegate:
// Оголошення типу делегата
public delegate void ProcessHandler(string message);
public delegate int CalculateHandler(int a, int b);
public delegate bool PredicateHandler<T>(T item);
Делегат визначає:
delegate void NotifyDelegate(string message);
class Program
{
static void EmailNotification(string msg)
{
Console.WriteLine($"📧 Email: {msg}");
}
static void SmsNotification(string msg)
{
Console.WriteLine($"📱 SMS: {msg}");
}
static void Main()
{
// Створення екземпляра делегата
NotifyDelegate notify = new NotifyDelegate(EmailNotification);
// Виклик делегата
notify("Ваше замовлення відправлено"); // 📧 Email: Ваше замовлення відправлено
}
}
delegate void NotifyDelegate(string message);
class Program
{
static void EmailNotification(string msg)
=> Console.WriteLine($"📧 Email: {msg}");
static void Main()
{
// Скорочений синтаксис
NotifyDelegate notify = EmailNotification;
notify("Ваше замовлення відправлено");
}
}
Делегати в C# підтримують Multicast — можливість викликати декілька методів послідовно:
delegate void LogDelegate(string message);
class Logger
{
static void ConsoleLog(string msg)
=> Console.WriteLine($"[CONSOLE] {msg}");
static void FileLog(string msg)
=> Console.WriteLine($"[FILE] {msg}");
static void Main()
{
LogDelegate log = ConsoleLog;
log += FileLog; // Додавання методу
log("Помилка підключення");
// Виведе:
// [CONSOLE] Помилка підключення
// [FILE] Помилка підключення
log -= ConsoleLog; // Видалення методу
log("Повторна спроба");
// Виведе тільки:
// [FILE] Повторна спроба
}
}
null. Виклик null делегата спричинить виняток. Використовуйте оператор ?.:log?.Invoke("Безпечний виклик");
Це механізм, який дозволяє використовувати методи, чия сигнатура не зовсім збігається з делегатом, але є сумісною з точки зору наслідування типів.
outДозволяє методу повертати більш конкретний тип (спадкоємець), ніж оголошено в делегаті.
"Якщо делегат обіцяє повернути
Person, то метод, що повертаєEmployee, теж підходить, боEmployee— це тежPerson."
inДозволяє методу приймати більш загальний тип параметра (батько), ніж оголошено в делегаті.
"Якщо делегат передає
Employee, то метод, який вміє обробляти будь-якогоPerson, теж впорається, боEmployeeєPerson."
Це пряме застосування принципу підстановки Лісков (LSP).
class Person { }
class Employee : Person { }
class Manager : Employee { }
delegate Person Factory(); // Covariant return
delegate void Handler(Employee e); // Contravariant param
class Program
{
// Повертає Manager (спадкоємець Person) -> OK
static Manager CreateManager() => new Manager();
// Приймає Person (батько Employee) -> OK
static void LogPerson(Person p) { }
static void Main()
{
// COVARIANCE: Factory очікує Person, ми даємо Manager.
// Це безпечно, бо хто очікує Person, зможе працювати з Manager.
Factory factory = CreateManager;
// CONTRAVARIANCE: Handler збирається передати Employee.
// Ми даємо метод, що приймає Person.
// Це безпечно, бо метод LogPerson точно зможе обробити Employee.
Handler handler = LogPerson;
}
}
Узагальнені делегати Func та Action вже мають правильні модифікатори варіативності:
Func<out TResult>: коваріантний за результатом.Action<in T>: контраваріантний за аргументом.Func<string> до Func<object>, це працює. Але Func<int> до Func<object> — ні, тому що int — це value type, і потребує boxing, що змінює представлення в пам'яті.Теоретичні аспекти варіативності в делегатах:
Варіативність у делегатах реалізована на рівні компілятора та CLR. Вона дозволяє забезпечити типобезпечність при використанні делегатів з різними, але сумісними типами. Коваріантність дозволяє використовувати більш похідні типи в позиціях, де очікуються базові типи, а контраваріантність дозволяє використовувати базові типи в позиціях, де очікуються більш похідні типи.
Ці механізми реалізовані через модифікатори in та out у визначенні узагальнених параметрів типу. Модифікатор out дозволяє коваріантність (використання параметра типу тільки в позиціях "на вихід" - повернення значень), а модифікатор in дозволяє контраваріантність (використання параметра типу тільки в позиціях "на вхід" - параметри методів).
Варіативність є важливим аспектом дизайну API та дозволяє створювати більш гнучкі та зручні у використанні інтерфейси, особливо при роботі з узагальненими типами даних.
Замість створення власних типів делегатів для кожного випадку, C# надає універсальні вбудовані делегати: Action, Func та Predicate.
Action — делегат, який не повертає значення (void). Може мати від 0 до 16 параметрів.
// Без параметрів
Action greet = () => Console.WriteLine("Привіт!");
// Один параметр
Action<string> printMessage = msg => Console.WriteLine(msg);
// Декілька параметрів
Action<string, int> printWithCode = (msg, code) =>
Console.WriteLine($"[{code}] {msg}");
greet(); // Привіт!
printMessage("Помилка системи"); // Помилка системи
printWithCode("Не знайдено", 404); // [404] Не знайдено
Func — делегат, який повертає значення. Останній параметр типу — це тип повернення.
// Без параметрів, повертає int
Func<int> getRandomNumber = () => Random.Shared.Next(1, 101);
// Один параметр, повертає результат
Func<int, int> square = x => x * x;
// Два параметри, повертає суму
Func<int, int, int> add = (a, b) => a + b;
// Перетворення типів
Func<string, int> parseToInt = s => int.Parse(s);
Console.WriteLine(getRandomNumber()); // Випадкове число
Console.WriteLine(square(5)); // 25
Console.WriteLine(add(10, 20)); // 30
Console.WriteLine(parseToInt("42")); // 42
Predicate<T> — спеціалізований делегат для перевірки умови. Приймає один параметр типу T та повертає bool.
Predicate<int> isEven = x => x % 2 == 0;
Predicate<string> isNullOrEmpty = string.IsNullOrEmpty;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(isEven(7)); // False
Console.WriteLine(isNullOrEmpty("")); // True
Console.WriteLine(isNullOrEmpty("text")); // False
// Використання з List.FindAll
List<int> numbers = [1, 2, 3, 4, 5, 6];
List<int> evenNumbers = numbers.FindAll(isEven);
Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
| Делегат | Сигнатура | Повертає значення? | Типове використання |
|---|---|---|---|
Action | void Method() | ❌ Ні | Виконання дій без результату |
Action<T> | void Method(T arg) | ❌ Ні | Обробка даних без повернення |
Func<TResult> | TResult Method() | ✅ Так | Генерація значень |
Func<T, TResult> | TResult Method(T arg) | ✅ Так | Трансформація даних |
Predicate<T> | bool Method(T arg) | ✅ Так (bool) | Перевірка умов, фільтрація |
Func<T1, T2, TResult> | TResult Method(T1 arg1, T2 arg2) | ✅ Так | Комбінування двох значень |
Comparison<T> | int Method(T x, T y) | ✅ Так | Порівняння елементів для сортування |
EventHandler | void Method(object sender, EventArgs e) | ❌ Ні | Обробка подій в UI та інших системах |
EventHandler<TEventArgs> | void Method(object sender, TEventArgs e) | ❌ Ні | Обробка подій з додатковою інформацією |
Action та Func замість створення власних типів делегатів, якщо це можливо. Це робить код більш читабельним та стандартизованим.Анонімні методи (Anonymous Methods) — це способ створення делегата без оголошення окремого іменованого методу. Введені в C# 2.0.
Func<int, int, int> sum = delegate(int a, int b)
{
return a + b;
};
Console.WriteLine(sum(5, 3)); // 8
class Calculator
{
static int Add(int a, int b)
{
return a + b;
}
static void Main()
{
Func<int, int, int> operation = Add;
Console.WriteLine(operation(10, 5)); // 15
}
}
class Calculator
{
static void Main()
{
Func<int, int, int> operation = delegate(int a, int b)
{
return a + b;
};
Console.WriteLine(operation(10, 5)); // 15
}
}
Анонімні методи дозволяють:
Лямбда-вирази (Lambda Expressions) — це сучасний та лаконічний спосіб створення анонімних функцій. Вони є еволюцією анонімних методів.
Базова форма лямбда-виразу:
(parameters) => expression-or-statement-block
// Один вираз справа від =>
Func<int, int> square = x => x * x;
// Еквівалентно:
Func<int, int> squareLong = x => { return x * x; };
// Блок операторів
Func<int, int, int> divide = (a, b) =>
{
if (b == 0)
throw new DivideByZeroException();
return a / b;
};
// Без параметрів
Action sayHello = () => Console.WriteLine("Hello!");
// Один параметр (дужки необов'язкові)
Func<int, int> double = x => x * 2;
// Два і більше параметрів (дужки обов'язкові)
Func<int, int, int> multiply = (a, b) => a * b;
// Явна типізація параметрів
Func<int, int> explicitType = (int x) => x + 1;
// Ігнорування параметрів (C# 9.0+)
Func<int, int, int> ignoreSecond = (x, _) => x * 2;
Компілятор може автоматично визначити тип делегата:
// Компілятор виведе тип Func<string, int>
var parse = (string s) => int.Parse(s);
// Використання
int number = parse("42");
Console.WriteLine(number); // 42
s має бути явно вказаний, інакше компілятор не зможе вивести тип parse.Замикання (Closure) — це механізм, що дозволяє лямбда-виразу "захоплювати" змінні з зовнішнього контексту та подовжувати їх час життя, навіть якщо зовнішній метод вже завершив виконання.
int multiplier = 5;
Func<int, int> multiplyByFive = x => x * multiplier;
Console.WriteLine(multiplyByFive(10)); // 50
multiplier = 10; // Змінюємо зовнішню змінну
Console.WriteLine(multiplyByFive(10)); // 100 — захоплена змінна!
Компілятор створює прихований клас (display class) для зберігання захоплених змінних.
this: Якщо лямбда використовує члени класу, вона захоплює this, що може подовжити життя всього об'єкта.// Ваш код:
class Calculator
{
int _base = 10;
public Func<int, int> GetMultiplier(int factor)
{
return x => x * factor + _base;
}
}
// Що генерує компілятор (спрощено):
class Calculator
{
int _base = 10;
// Display Class для методу GetMultiplier
sealed class <>c__DisplayClass0_0
{
public int factor; // Захоплений локальний параметр
public Calculator <>4__this; // Захоплене посилання на батьківський об'єкт
public int <GetMultiplier>b__0(int x)
{
// Доступ до _base через захоплений this
return x * factor + <>4__this._base;
}
}
public Func<int, int> GetMultiplier(int factor)
{
var closure = new <>c__DisplayClass0_0();
closure.factor = factor;
closure.<>4__this = this; // УВАГА: Calculator не буде зібраний GC, поки живе делегат!
return new Func<int, int>(closure.<GetMultiplier>b__0);
}
}
До C# 5.0 змінна циклу foreach оголошувалася зовні циклу. Це призводило до того, що всі лямбди захоплювали одну й ту ж змінну.
var actions = new List<Action>();
for (int i = 0; i < 3; i++) // 'i' одна на весь цикл
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var a in actions) a();
// Виведе: 3, 3, 3 (значення i після завершення циклу)
Чому так? Компілятор створює один екземпляр display class перед циклом, і в циклі просто оновлює його поле. Всі делегати посилаються на цей єдиний об'єкт.
Рішення: Створити локальну копію всередині блоку циклу.
for (int i = 0; i < 3; i++)
{
int copy = i; // Нова змінна на кожній ітерації -> новий display class
actions.Add(() => Console.WriteLine(copy));
}
foreach автоматично створює нову змінну на кожній ітерації, тому там ця проблема зникла. Але для for вона все ще актуальна!Лямбда-вирази не можуть захоплювати параметри ref, out або in.
Причина: Ці параметри живуть на стеку (або є посиланнями на стек). Display class створюється в кучі (heap). Посилання з кучі на стек заборонені в .NET заради безпеки пам'яті (щоб уникнути доступу до звільненого фрейму стеку).
void TryCapture(ref int x)
{
// Помилка компіляції CS1628: Cannot use ref, out, or in parameter inside an anonymous method
Action act = () => Console.WriteLine(x);
}
Починаючи з C# 10, можна додавати атрибути до лямбда-виразів:
var parse = [return: NotNull] (string s) => int.Parse(s);
Func<string, int> parseWithAttribute =
[Obsolete("Use TryParse instead")]
(string s) => int.Parse(s);
Лямбда-вирази можуть бути перетворені на дерева виразів (Expression<TDelegate>), що дозволяє аналізувати їх як дані:
Expression<Func<int, int>> expression = x => x * x;
Console.WriteLine(expression); // x => (x * x)
Теоретичні аспекти дерева виразів:
Дерева виразів представляють код у вигляді структури даних, яку можна аналізувати, модифікувати та перетворювати. На відміну від звичайних делегатів, які містять виконуваний IL-код, дерева виразів зберігають структуру самого виразу у вигляді об'єктів.
Коли ви створюєте Expression<TDelegate>, компілятор генерує не виконуваний код, а структуру об'єктів, що представляє вираз. Ця структура може бути:
Кожен вузол дерева виразу представляє елемент мови C# (наприклад, змінну, метод, оператор). Це дозволяє розробляти потужні системи, які можуть аналізувати та перетворювати код під час виконання, що особливо корисно для ORM, які перетворюють LINQ-запити в SQL.
Події (Events) — це механізм, що дозволяє об'єктам повідомляти інші об'єкти про зміни стану або важливі події. Вони базуються на Publisher-Subscriber pattern (паттерн "Видавець-Підписник").
// 1. Оголосити делегат для події
public delegate void NotifyEventHandler(string message);
// Клас-видавець
public class FileProcessor
{
// 2. Оголосити подію за допомогою ключового слова event
public event NotifyEventHandler? FileProcessed;
public void ProcessFile(string filename)
{
Console.WriteLine($"Обробка файлу: {filename}");
// Симуляція обробки...
// 3. Викликати подію (raising event)
FileProcessed?.Invoke($"Файл {filename} оброблено");
}
}
// Клас-підписник
public class Logger
{
public void OnFileProcessed(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
// Використання
class Program
{
static void Main()
{
var processor = new FileProcessor();
var logger = new Logger();
// 4. Підписатися на подію
processor.FileProcessed += logger.OnFileProcessed;
processor.ProcessFile("data.txt");
// Виведе:
// Обробка файлу: data.txt
// [LOG] Файл data.txt оброблено
}
}
.NET має стандартний паттерн для подій: використання EventHandler делегата та EventArgs:
// Власний клас аргументів події
public class FileProcessedEventArgs : EventArgs
{
public string FileName { get; }
public long FileSize { get; }
public FileProcessedEventArgs(string fileName, long fileSize)
{
FileName = fileName;
FileSize = fileSize;
}
}
// Клас-видавець
public class FileProcessor
{
// Використання EventHandler<T>
public event EventHandler<FileProcessedEventArgs>? FileProcessed;
public void ProcessFile(string filename)
{
Console.WriteLine($"Обробка файлу: {filename}");
long fileSize = 1024; // Симуляція
// Виклик події з аргументами
OnFileProcessed(new FileProcessedEventArgs(filename, fileSize));
}
// Protected virtual метод для виклику події (best practice)
protected virtual void OnFileProcessed(FileProcessedEventArgs e)
{
FileProcessed?.Invoke(this, e);
}
}
// Підписник
class Program
{
static void Main()
{
var processor = new FileProcessor();
// Підписка з використанням лямбда-виразу
processor.FileProcessed += (sender, e) =>
{
Console.WriteLine($"Оброблено: {e.FileName} ({e.FileSize} bytes)");
};
processor.ProcessFile("document.pdf");
// Виведе:
// Обробка файлу: document.pdf
// Оброблено: document.pdf (1024 bytes)
}
}
protected virtual метод OnEventName для виклику події. Це дозволяє похідним класам перевизначати поведінку події.protected virtual метод OnEventName для виклику події. Це дозволяє похідним класам перевизначати поведінку події.У C# є два способи оголошення подій:
public event EventHandler Click;
```
Компілятор автоматично створює приватне поле-делегат і методи `add`/`remove`.
2. **Explicit events** (розгорнутий синтаксис):
Ви самі керуєте зберіганням делегата (наприклад, використовуючи `EventHandlerList` для економії пам'яті, якщо подій дуже багато, а підписників мало).
### Потокобезпечність (Thread Safety)
Field-like події є потокобезпечними за замовчуванням. Компілятор генерує методи `add` та `remove` з використанням `Interlocked.CompareExchange`.
Ось як виглядає код, який генерує компілятор для `add`:
```csharp
public void add_Click(EventHandler value)
{
EventHandler loop, current = this.Click;
do
{
loop = current;
// Об'єднуємо поточний список з новим делегатом
EventHandler combined = (EventHandler)Delegate.Combine(loop, value);
// Атомарно замінюємо старе значення новим, якщо ніхто не встиг змінити його
current = Interlocked.CompareExchange(ref this.Click, combined, loop);
}
while (current != loop); // Якщо під час операції поле змінилося — повторюємо
}
Це гарантує, що якщо два потоки одночасно підписуються, жодна підписка не загубиться.
Click != null, а потік Б в цей момент відписав останнього підписника (зробивши Click null), то потік А впаде з NullReferenceException.
Рішення: Click?.Invoke(...) або копіювання в локальну змінну var handler = Click; handler(...).Типова проблема подій — Lapsed Listener Problem. Якщо довгоживучий об'єкт (Publisher) має посилання на короткоживучий об'єкт (Subscriber) через подію, то Subscriber не буде зібраний GC, поки живе Publisher.
Рішення:
-=) у Dispose().WeakReference або спеціальні бібліотеки (наприклад, WeakEventManager у WPF).Теоретичні аспекти продуктивності делегатів:
Використання делегатів та лямбда-виразів має певні витрати на продуктивність, які важливо враховувати:
Теоретичні аспекти потокобезпеки та продуктивності:
Під капотом, безпечна підписка/відписка від подій використовує алгоритм CAS (Compare-And-Swap), який є неблокуючим. Це означає, що коли кілька потоків одночасно намагаються змінити список підписників, вони не блокуються, а замість цього повторюють операцію до успішного завершення.
Проте, це створює певні накладні витрати на продуктивність:
Для високопродуктивних сценаріїв існують альтернативні підходи, такі як використання lock-free черг подій або спеціалізованих бібліотек для управління подіями.
Іноді потрібен більший контроль над підпискою/відпискою:
public class Button
{
private EventHandler? _click;
public event EventHandler Click
{
add
{
Console.WriteLine("Підписка на Click");
_click += value;
}
remove
{
Console.WriteLine("Відписка від Click");
_click -= value;
}
}
public void SimulateClick()
{
_click?.Invoke(this, EventArgs.Empty);
}
}
public class Calculator
{
public int Execute(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}
}
class Program
{
static void Main()
{
var calc = new Calculator();
// Різні операції як делегати
Func<int, int, int> add = (x, y) => x + y;
Func<int, int, int> subtract = (x, y) => x - y;
Func<int, int, int> multiply = (x, y) => x * y;
Console.WriteLine(calc.Execute(10, 5, add)); // 15
Console.WriteLine(calc.Execute(10, 5, subtract)); // 5
Console.WriteLine(calc.Execute(10, 5, multiply)); // 50
// Власна операція
Console.WriteLine(calc.Execute(10, 5, (x, y) => x % y)); // 0
}
}
// Subject (Observable)
public class Stock
{
private decimal _price;
public string Symbol { get; }
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
OnPriceChanged(new PriceChangedEventArgs(_price));
}
}
}
public event EventHandler<PriceChangedEventArgs>? PriceChanged;
public Stock(string symbol, decimal initialPrice)
{
Symbol = symbol;
_price = initialPrice;
}
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
}
public class PriceChangedEventArgs : EventArgs
{
public decimal NewPrice { get; }
public PriceChangedEventArgs(decimal newPrice)
{
NewPrice = newPrice;
}
}
// Observer
public class StockMonitor
{
public void Subscribe(Stock stock)
{
stock.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(object? sender, PriceChangedEventArgs e)
{
if (sender is Stock stock)
{
Console.WriteLine($"{stock.Symbol}: {e.NewPrice:C}");
}
}
}
// Використання
class Program
{
static void Main()
{
var appleStock = new Stock("AAPL", 150.00m);
var monitor = new StockMonitor();
monitor.Subscribe(appleStock);
appleStock.Price = 155.50m; // AAPL: $155.50
appleStock.Price = 160.00m; // AAPL: $160.00
}
}
Action myAction = null;
myAction(); // ❌ NullReferenceException
// Використання null-conditional operator
myAction?.Invoke();
// Або перевірка перед викликом
if (myAction != null)
{
myAction();
}
public class LongLivedPublisher
{
public event EventHandler? SomeEvent;
}
public class ShortLivedSubscriber
{
public ShortLivedSubscriber(LongLivedPublisher publisher)
{
publisher.SomeEvent += OnSomeEvent;
// ❌ Підписник ніколи не звільниться!
}
private void OnSomeEvent(object? sender, EventArgs e) { }
}
public class ShortLivedSubscriber : IDisposable
{
private readonly LongLivedPublisher _publisher;
public ShortLivedSubscriber(LongLivedPublisher publisher)
{
_publisher = publisher;
_publisher.SomeEvent += OnSomeEvent;
}
public void Dispose()
{
_publisher.SomeEvent -= OnSomeEvent;
}
private void OnSomeEvent(object? sender, EventArgs e) { }
}
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
actions[0](); // 5, а не 0!
actions[4](); // 5
for (int i = 0; i < 5; i++)
{
int localI = i; // Локальна копія
actions.Add(() => Console.WriteLine(localI));
}
actions[0](); // 0
actions[4](); // 4
Func<int> getNumber = () => 1;
getNumber += () => 2;
getNumber += () => 3;
int result = getNumber(); // result = 3 (тільки останній!)
Func<int> getNumber = () => 1;
getNumber += () => 2;
getNumber += () => 3;
var results = new List<int>();
foreach (Func<int> func in getNumber.GetInvocationList())
{
results.Add(func());
}
Console.WriteLine(string.Join(", ", results)); // 1, 2, 3
Завдання: Створіть делегат MathOperation, який приймає два int та повертає int. Напишіть методи для додавання, віднімання та множення. Викличте їх через делегат.
Рішення:
delegate int MathOperation(int a, int b);
class Program
{
static int Add(int a, int b) => a + b;
static int Subtract(int a, int b) => a - b;
static int Multiply(int a, int b) => a * b;
static void Main()
{
MathOperation op;
op = Add;
Console.WriteLine($"10 + 5 = {op(10, 5)}"); // 15
op = Subtract;
Console.WriteLine($"10 - 5 = {op(10, 5)}"); // 5
op = Multiply;
Console.WriteLine($"10 * 5 = {op(10, 5)}"); // 50
}
}
Завдання: Використайте Action<string> для виведення повідомлення та Func<int, int, int> для знаходження максимуму двох чисел.
Рішення:
class Program
{
static void Main()
{
// Action для виведення
Action<string> print = msg => Console.WriteLine($">> {msg}");
print("Привіт, світ!");
// Func для знаходження максимуму
Func<int, int, int> max = (a, b) => a > b ? a : b;
Console.WriteLine($"Максимум з 10 та 20: {max(10, 20)}"); // 20
}
}
Завдання: Створіть клас TemperatureMonitor з подією TemperatureChanged. Коли температура перевищує 30°C, викликайте подію. Створіть підписника, який виводить попередження.
Рішення:
public class TemperatureChangedEventArgs : EventArgs
{
public double Temperature { get; }
public TemperatureChangedEventArgs(double temperature)
{
Temperature = temperature;
}
}
public class TemperatureMonitor
{
private double _temperature;
public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;
public double Temperature
{
get => _temperature;
set
{
_temperature = value;
if (_temperature > 30)
{
OnTemperatureChanged(new TemperatureChangedEventArgs(_temperature));
}
}
}
protected virtual void OnTemperatureChanged(TemperatureChangedEventArgs e)
{
TemperatureChanged?.Invoke(this, e);
}
}
class Program
{
static void Main()
{
var monitor = new TemperatureMonitor();
monitor.TemperatureChanged += (sender, e) =>
{
Console.WriteLine($"⚠️ Попередження! Температура: {e.Temperature}°C");
};
monitor.Temperature = 25; // Нічого
monitor.Temperature = 35; // ⚠️ Попередження! Температура: 35°C
}
}
Завдання: Створіть систему логування, яка використовує multicast делегат для виведення в консоль та "файл" (просто інший Console.WriteLine з префіксом FILE).
Рішення:
delegate void LogHandler(string message);
class Program
{
static void ConsoleLog(string msg)
=> Console.WriteLine($"[CONSOLE] {msg}");
static void FileLog(string msg)
=> Console.WriteLine($"[FILE] {msg}");
static void Main()
{
LogHandler logger = ConsoleLog;
logger += FileLog;
logger("Система завантажена");
// Виведе:
// [CONSOLE] Система завантажена
// [FILE] Система завантажена
logger -= ConsoleLog;
logger("Тільки у файл");
// Виведе:
// [FILE] Тільки у файл
}
}
Завдання: Створіть клас Button з подією Click, яка має custom accessors. Лічильник підписок має обмежувати кількість підписників до 3. При спробі підписатися вчетверте — викидати виняток.
Рішення:
public class Button
{
private EventHandler? _click;
private int _subscriberCount = 0;
private const int MaxSubscribers = 3;
public event EventHandler Click
{
add
{
if (_subscriberCount >= MaxSubscribers)
{
throw new InvalidOperationException(
$"Максимум {MaxSubscribers} підписників дозволено");
}
_click += value;
_subscriberCount++;
Console.WriteLine($"Підписників: {_subscriberCount}");
}
remove
{
_click -= value;
_subscriberCount--;
Console.WriteLine($"Підписників: {_subscriberCount}");
}
}
public void SimulateClick()
{
_click?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var button = new Button();
EventHandler handler1 = (s, e) => Console.WriteLine("Handler 1");
EventHandler handler2 = (s, e) => Console.WriteLine("Handler 2");
EventHandler handler3 = (s, e) => Console.WriteLine("Handler 3");
EventHandler handler4 = (s, e) => Console.WriteLine("Handler 4");
button.Click += handler1; // Підписників: 1
button.Click += handler2; // Підписників: 2
button.Click += handler3; // Підписників: 3
try
{
button.Click += handler4; // ❌ Exception
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Помилка: {ex.Message}");
}
button.SimulateClick();
// Handler 1
// Handler 2
// Handler 3
}
}
Завдання: Створіть метод розширення Filter<T> для IEnumerable<T>, який приймає Predicate<T> та повертає відфільтровані елементи.
Рішення:
public static class EnumerableExtensions
{
public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Predicate<T> predicate)
{
foreach (var item in source)
{
if (predicate(item))
{
yield return item;
}
}
}
}
class Program
{
static void Main()
{
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Фільтр парних чисел
var evenNumbers = numbers.Filter(x => x % 2 == 0);
Console.WriteLine("Парні: " + string.Join(", ", evenNumbers));
// Парні: 2, 4, 6, 8, 10
// Фільтр чисел > 5
var greaterThanFive = numbers.Filter(x => x > 5);
Console.WriteLine("Більше 5: " + string.Join(", ", greaterThanFive));
// Більше 5: 6, 7, 8, 9, 10
}
}
Делегати
Action & Func
Action для void методів, Func для методів з результатом.Лямбда-вирази
Події
EventHandler<T> та EventArgs.Узагальнююча таблиця концепцій:
| Концепція | Призначення | Приклад використання | Переваги | Недоліки |
|---|---|---|---|---|
| Делегати | Типобезпечні покажчики на методи | Func<int, int> square = x => x * x; | Типобезпека, гнучкість | Невеликі накладні витрати на виклик |
| Multicast делегати | Виклик декількох методів | logger += ConsoleLog; logger += FileLog; | Можливість одночасного виклику кількох методів | Повертається лише результат останнього методу |
| Лямбда-вирази | Лаконічний синтаксис для анонімних функцій | numbers.Where(x => x > 5) | Компактність, виведення типів | Можуть створювати замикання, що впливає на GC |
| Події | Паттерн Publisher-Subscriber | button.Click += OnButtonClick; | Loose coupling, безпечне сповіщення | Потенційні витоки пам'яті при неправильному використанні |
| Expression Trees | Аналіз та перетворення коду в структуру | Expression<Func<int, int>> expr = x => x * 2; | Можливість аналізу та перетворення виразів | Додаткові витрати на компіляцію назад у делегат |