Підрозділ 6.5
Делегати Action, Predicate та Func
Розглядає вбудовані делегати Action, Predicate і Func, а також замикання через локальні функції та лямбда-вирази.
6.5. Делегати Action, Predicate та Func
У .NET є кілька вбудованих узагальнених делегатів, які покривають переважну більшість сценаріїв використання делегатів без потреби оголошувати власні типи. Найбільш вживані з них — Action, Predicate та Func. Їх наявність у стандартній бібліотеці дозволяє писати компактний код: замість delegate void AlertHandler(string msg) достатньо написати Action<string>, і компілятор розуміє те ж саме.
Action
Делегат Action представляє дію, яка нічого не повертає (void). Він має кілька перевантажених версій залежно від кількості параметрів: від Action (без параметрів) до Action<T1, T2, …, T16> (до 16 параметрів):
public delegate void Action()
public delegate void Action<in T>(T obj)
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2)
// ... до Action<T1..T16>Action зазвичай передається як параметр методу і описує реакцію на якусь подію. Наприклад, метод обробки показника пацієнта може приймати Action як параметр і виконати потрібну дію залежно від контексту:
Метод ProcessVitals не містить жодної логіки виведення — він лише викликає дію report, яку передає зовнішній код. Це дає гнучкість: в одному місці передаємо LogAlert, в іншому — LogNormal, або будь-яку лямбду.
Action без параметрів зручний для реєстрації простих зворотних викликів:
Predicate
Делегат Predicate<T> приймає один параметр типу T і повертає bool. Він описує умову — відповідає або не відповідає об'єкт певному критерію:
delegate bool Predicate<in T>(T obj);Predicate використовується для фільтрації, перевірки, пошуку. Наприклад, перевіримо кілька показників пацієнта на відповідність нормі:
Predicate<T> можна передавати у методи як параметр фільтрації:
Func
Делегат Func повертає значення і може приймати параметри. Останній параметр типу завжди є типом повернення (позначається TResult):
Func<TResult> // без параметрів, повертає TResult
Func<T, TResult> // один параметр T, повертає TResult
Func<T1, T2, TResult> // два параметри, повертає TResult
// ... до Func<T1..T16, TResult>Func часто використовується для обчислень і перетворень. Наприклад, набір функцій обробки медичних показників:
Func зручний також як параметр методу — для стратегій обчислення:

Замикання
Замикання (closure) — це функція, яка «запам'ятовує» своє лексичне оточення навіть тоді, коли виконується поза областю видимості, де вона була створена. Якщо звичайна функція звертається лише до своїх параметрів і локальних змінних, то замикання також має доступ до змінних зовнішньої функції, у якій воно визначене.
Технічно замикання складається з трьох компонентів:
- зовнішня функція — визначає область видимості і змінні (лексичне оточення)
- лексичне оточення — змінні та параметри зовнішньої функції
- вкладена функція — захоплює і використовує лексичне оточення
У C# замикання реалізуються через локальні функції та лямбда-вирази.
Замикання через локальні функції
Тут CreateAlertCounter повертає локальну функцію Increment. Після виходу з CreateAlertCounter змінна count більше не існує «звичайним чином» на стеку — але замикання зберігає її в купі (heap). Кожен виклик counter() читає і змінює саме цю захоплену змінну, тому лічильник зростає між викликами.
Замикання через лямбда-вирази
За допомогою лямбд можна скоротити визначення замикання:
Кожен виклик createCounter створює нове замикання з власним незалежним count. icuCounter і cardCounter — два окремих замикання, кожне з яких зберігає свій власний лічильник.
Застосування параметрів у замиканнях
До лексичного оточення замикання належать не лише локальні змінні, а й параметри зовнішньої функції. Це дозволяє створювати «фабрики функцій» — методи, що повертають налаштовані дії:
Метод CreateRangeCheck приймає два порогові значення і повертає лямбду, яка їх «запам'ятовує». glucoseCheck і cholesterolCheck — це два різних замикання, кожне з яких містить свої пороги. Замість передавати low і high при кожному виклику — вони «вбудовані» у функцію один раз при її створенні.

Скорочений запис через каррінг (ланцюг лямбд):
Замикання — потужний механізм, але він несе відповідальність: захоплена змінна живе у пам'яті доти, доки живе замикання. Якщо замикання зберігається довго (наприклад, підписане на подію, що ніколи не відписується), захоплені великі об'єкти не звільняться збирачем сміття. При роботі з замиканнями слід усвідомлювати, які саме змінні захоплюються і який час вони мають жити.