JavaRush /Курсы /C# SELF /Функции высшего порядка

Функции высшего порядка

C# SELF
51 уровень , 2 лекция
Открыта

1. Введение

Наверняка в школе вам говорили: "Это функция от функции". Функции высшего порядка (Higher-Order Functions, HOF) — это функции, которые либо принимают функции как аргументы, либо возвращают функции как результат, либо и то, и другое.

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

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

Применение в реальной жизни

Это всё звучит интересно и нетривиально. Только вот зачем такие кульбиты нужны в будничной жизни разработчика на C#? Вот самый короткий ответ:
Функции высшего порядка делают код гибким, переиспользуемым и лаконичным.
Это основа для таких вещей, как LINQ, фильтрация и сортировка коллекций, построение пайплайнов обработки данных, настройка обратных вызовов, событий и даже внедрение зависимостей.

Несколько реальных сценариев:

  • Ограничить поведение метода, передав туда логику (например, фильтрацию, сортировку, преобразование).
  • Написать универсальный обработчик данных, которому вы "вкручиваете" нужную операцию через делегат.
  • Построить цепочки обработки ("пайплайны"), где каждая функция модифицирует данные по своему рецепту.
  • Создавать кроссплатформенные абстракции: что делать в 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.

Захват переменных (closures).
Если возвращаемая функция использует переменные из внешнего окружения, следите, чтобы значения были актуальны на момент вызова. Переменные не копируются, а "захватываются".

Отладка сложных цепочек.
Когда функции компонуются в длинные пайплайны, становится сложно понять, какой слой обработал данные не так. Добавляйте временные выводы и комментарии:


n => {
  Console.WriteLine("До операции:" + n);
  var res = n * 2;
  Console.WriteLine("После:" + res);
  return res;
}
2
Задача
C# SELF, 51 уровень, 2 лекция
Недоступна
Фильтрация с помощью функций высшего порядка
Фильтрация с помощью функций высшего порядка
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ