JavaRush /Курси /C# SELF /Функції вищого порядку

Функції вищого порядку

C# SELF
Рівень 51 , Лекція 2
Відкрита

1. Вступ

Напевно, у школі вам казали: «Це функція від функції». Функції вищого порядку (Higher-Order Functions, HOF) — це функції, які приймають інші функції як аргументи, повертають функції як результат або роблять і те, і інше.

Простими словами: якщо ваш метод може отримати як параметр іншу функцію (наприклад, делегат або лямбду) чи повернути її як результат — вітаємо, у вас функція вищого порядку!

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

Застосування в реальному житті

Усе це звучить цікаво й нетривіально. Але навіщо такі прийоми потрібні в повсякденній роботі розробника у C#? Ось найкоротша відповідь:
Функції вищого порядку роблять код гнучким, багаторазово придатним і лаконічним.
Це основа для таких речей, як LINQ, фільтрація та сортування колекцій, побудова пайплайнів обробки даних, налаштування callback-ів, подій і навіть інʼєкції залежностей.

Кілька реальних сценаріїв:

  • Обмежити поведінку методу, передавши туди потрібну логіку (наприклад, фільтрацію, сортування, перетворення).
  • Написати універсальний обробник даних, у який ви вбудовуєте потрібну операцію через делегат.
  • Побудувати ланцюжки обробки («пайплайни»), де кожна функція модифікує дані за власним рецептом.
  • Створювати кросплатформенні абстракції: що робити у Windows, що — у Linux? Просто передайте потрібну функцію.

2. Приклади простих функцій вищого порядку

Функція, що приймає іншу функцію

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


// Функція вищого порядку: приймає функцію process як параметр
void ForEach<T>(IEnumerable<T> collection, Action<T> process)
{
    foreach (var item in collection)
    {
        process(item); // Викликаємо функцію-аргумент
    }
}

// Використання:
var numbers = new List<int> { 1, 2, 3 };
ForEach(numbers, n => Console.WriteLine($"Елемент: {n}"));

Що тут відбувається? Метод ForEach не знає, що саме робити з кожним елементом. Усе, що він робить, — викликає переданий процесор (process). Цей процесор може бути будь-яким — друкувати на екран, зберігати до бази, малювати на екрані тощо.

Так — саме так працює метод ForEach у колекціях C#, і майже всі методи LINQ — це функції вищого порядку!

Функція, що повертає функцію

А тепер приклад складніший — метод, який не лише приймає, а й повертає функцію.


// Функція вищого порядку: повертає іншу функцію
Func<int, int> CreateMultiplier(int factor)
{
    // Повертаємо лямбду, що використовує змінну factor
    return x => x * factor;
}

// Використання:
var multiplyBy10 = CreateMultiplier(10);
Console.WriteLine(multiplyBy10(7)); // 70

Тут CreateMultiplier повертає функцію, що множить свій аргумент на заздалегідь заданий множник. Це вже не просто HOF, а приклад «фабрики функцій».

Функція, що приймає і повертає функцію


Func<int, int> Compose(Func<int, int> f, Func<int, int> g)
{
    // Поверне функцію, що застосовує спочатку g, а потім f: f(g(x))
    return x => f(g(x));
}

// Використання:
Func<int, int> increment = x => x + 1;
Func<int, int> doubleIt = x => x * 2;

var incrementThenDouble = Compose(doubleIt, increment);

Console.WriteLine(incrementThenDouble(5)); // (5 + 1) * 2 = 12

Саме такі композиції лежать в основі конвеєрної обробки даних — наприклад, коли ви обробляєте масив методами Select, Where, OrderBy тощо.

3. Як C# підтримує функції вищого порядку

У функціональних мовах (Haskell, F#) усі функції — вищого порядку за замовчуванням. Але й C# (починаючи з версії 2.0) підтримує цей підхід завдяки делегатам і лямбдам.

  • Делегати (Func, Action, Predicate) — типи функцій.
  • Лямбда-вирази — синтаксис для створення функцій просто на місці.
  • Методи можуть приймати й повертати делегати — отже, функції вищого порядку підтримуються «з коробки».

Візуальна схема

flowchart LR
    A[Дані] --> B[Функція 1]
    B --> C[Функція 2]
    C --> D[Результат]
    subgraph "Пайплайн обробки (функції вищого порядку)"
        B
        C
    end

4. Розвиваємо наш застосунок

Продовжімо розвивати наш покроковий демозастосунок — нехай це буде «Міні-обробник рядків».

Додамо простий метод вищого порядку

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


// Метод, що приймає список рядків і функцію для перетворення
List<string> TransformNames(List<string> names, Func<string, string> transformer)
{
    var result = new List<string>();
    foreach (var name in names)
    {
        result.Add(transformer(name));
    }
    return result;
}

Як використовувати цей метод?


var names = new List<string> { "Анна", "Борис", "Сергій" };

// Перетворюємо у верхній регістр
var upperNames = TransformNames(names, n => n.ToUpper());

// Додаймо «пан/пані» до кожного імені
var politeNames = TransformNames(names, n => "Шановний(а) " + n);

foreach (var n in upperNames)
    Console.WriteLine(n); // АННА, БОРИС, СЕРГІЙ

foreach (var n in politeNames)
    Console.WriteLine(n); // Шановний(а) Анна, ...

Метод TransformNames універсальний: він делегує логіку перетворення переданому параметрові (transformer), наприклад виклику ToUpper або будь-якому іншому рецепту.

Адаптація до типів

Наш приклад легко адаптувати для будь-яких типів даних.


// Універсальний типовий метод — функція вищого порядку, що працює з будь-яким T
List<TResult> Map<T, TResult>(List<T> items, Func<T, TResult> transformer)
{
    var result = new List<TResult>();
    foreach (var item in items)
    {
        result.Add(transformer(item));
    }
    return result;
}

Застосування:


var numbers = new List<int> { 1, 2, 3 };
var doubled = Map(numbers, x => x * 2); // [2, 4, 6]
var strings = Map(numbers, x => $"Число: {x}"); // ["Число: 1", ...]

5. Фільтрація й агрегація через функції вищого порядку

Логіку фільтрації й пошуку вже давно реалізують через функції вищого порядку.


// Фільтрація: функція вищого порядку
List<T> Filter<T>(List<T> items, Predicate<T> criteria)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (criteria(item)) // Викликаємо функцію-критерій
        {
            result.Add(item);
        }
    }
    return result;
}

Як використовувати:


var names = new List<string> { "Анна", "Борис", "Андрій" };
var aNames = Filter(names, n => n.StartsWith("А"));
// Результат: "Анна", "Андрій"

6. Поняття композиції функцій (function composition)

Функції вищого порядку дозволяють не лише використовувати окремі функції, а й збирати їх у ланцюжки — так звані композиції. У C# це можна реалізувати як функцію, що приймає дві функції й повертає нову — їхню комбінацію.


// Композитор функцій: повертає функцію, яка застосовує спочатку f, потім g
Func<T, TResult> Compose<T, TIntermediate, TResult>(
    Func<TIntermediate, TResult> f,
    Func<T, TIntermediate> g)
{
    return x => f(g(x));
}

// Приклад:
Func<int, int> plusOne = n => n + 1;
Func<int, int> timesTwo = n => n * 2;

var plusOneThenDouble = Compose(timesTwo, plusOne);
Console.WriteLine(plusOneThenDouble(3)); // (3 + 1) * 2 = 8

7. Корисні нюанси

Передісторія проблеми: чому раніше все було складніше?

До появи делегатів і лямбд розробники часто писали безліч однотипних циклів, копіювали шматки коду, щоб «відфільтрувати», «змінити» або «згрупувати» дані. Із появою функцій вищого порядку стало можливим винести змінну частину поведінки в окремі функції-параметри — і таким чином радикально скоротити дублювання та підвищити виразність коду.

Трохи синтаксичного цукру: функції як вирази

Функції вищого порядку часто реалізують через expression-bodied методи — лаконічні однорядкові методи:


List<string> FilterNames(Predicate<string> pred) =>
    Names.Where(name => pred(name)).ToList();

List<TResult> MapNames<TResult>(Func<string, TResult> transformer) =>
    Names.Select(transformer).ToList();

Порівняння з механізмами LINQ

Подивімося, як LINQ використовує функції вищого порядку:

Метод LINQ Який делегат приймає Призначення
Where
Func<T, bool>
Фільтрує елементи
Select
Func<T, TResult>
Перетворює елементи
OrderBy
Func<T, TKey>
Сортує за ключем
Aggregate
Func<TAcc, T, TAcc>
Агрегує (згортає) колекцію
Any
Func<T, bool>
Перевіряє наявність елемента за умовою

Усі ці методи побудовані навколо ідеї функцій вищого порядку: ви формулюєте власні правила, а стандартна бібліотека забезпечує «інфраструктуру».

8. Можливі помилки та підводні камені

Плутанина із типами делегатів.
Спочатку може бути складно розібратися, де потрібен Action, де Func, а де Predicate.
Підказка: якщо функція повертає bool — найімовірніше, це Predicate. Якщо повертає значення — використовуйте Func, якщо нічого не повертає — Action.

Захоплення змінних (замикання).
Якщо повернена функція використовує змінні із зовнішнього оточення, стежте, щоб значення були актуальні на момент виклику. Змінні не копіюються, а «захоплюються».

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


n => {
  Console.WriteLine("До операції:" + n);
  var res = n * 2;
  Console.WriteLine("Після:" + res);
  return res;
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ