Функциональное программирование в C#: продвинутые техники и паттерны

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в C#: от синтаксиса к архитектуре

Современный C# давно вышел за рамки чисто объектно-ориентированного языка. Начиная с версии 3.0 (LINQ) и заканчивая последними релизами .NET, язык активно впитывает парадигмы функционального программирования (ФП). Если вы освоили лямбды и LINQ, но хотите копнуть глубже — эта статья для вас. Мы разберём продвинутые концепции ФП, которые сделают ваш код на C# более предсказуемым, тестируемым и лаконичным.

1. Чистые функции и работа с побочными эффектами

Чистая функция — это функция, которая:

  • Всегда возвращает один и тот же результат для одних и тех же аргументов (ссылочная прозрачность).
  • Не имеет побочных эффектов (не изменяет глобальное состояние, не пишет в файл, не посылает HTTP-запросы).

В C# идеальный пример чистой функции — математические операции. Проблема в том, что реальное приложение не может состоять только из чистых функций: рано или поздно нужно сохранить данные или отправить письмо. Решение — отделение логики от эффектов.

Продвинутый приём: используйте функторы и монады для контейнеризации эффектов. Рассмотрим простой пример с Maybe<T> (аналог nullable, но функциональный):

public class Maybe<T>
{
    private readonly T _value;
    public bool HasValue { get; }
    
    private Maybe(T value, bool hasValue) => (_value, HasValue) = (value, hasValue);
    
    public static Maybe<T> Some(T value) => new Maybe<T>(value, true);
    public static Maybe<T> None() => new Maybe<T>(default, false);
    
    // Функтор: Select (Map)
    public Maybe<TResult> Select<TResult>(Func<T, TResult> selector) =>
        HasValue ? Maybe<TResult>.Some(selector(_value)) : Maybe<TResult>.None();
    
    // Монада: SelectMany (FlatMap/Bind)
    public Maybe<TResult> SelectMany<TResult>(Func<T, Maybe<TResult>> binder) =>
        HasValue ? binder(_value) : Maybe<TResult>.None();
}

// Использование
Maybe<int> result = Maybe<int>.Some(10)
    .Select(x => x * 2)
    .SelectMany(x => x > 15 ? Maybe<int>.Some(x) : Maybe<int>.None());

Этот код не генерирует исключений и не проверяет null вручную — вся логика обработки отсутствия значения инкапсулирована в монаде.

2. Каррирование и частичное применение

В классическом ФП каждая функция принимает один аргумент. Каррирование — это преобразование функции с несколькими параметрами в цепочку вложенных функций, каждая из которых принимает один аргумент. Частичное применение — фиксация части аргументов.

В C# нет встроенного каррирования, но его легко реализовать через замыкания:

// Обычная функция
Func<int, int, int, int> add = (a, b, c) => a + b + c;

// Каррированная версия
Func<int, Func<int, Func<int, int>>> curriedAdd = a => b => c => a + b + c;

// Частичное применение: фиксируем первый аргумент
Func<int, Func<int, int>> add5 = curriedAdd(5);

// Использование
int sum = add5(10)(20); // 35

Продвинутый сценарий: внедрение зависимостей через частичное применение. Вместо того чтобы передавать конфигурацию или сервис через конструктор класса, можно зафиксировать их на этапе композиции:

public class ReportService
{
    public Func<string, string, string> FormatReport => 
        (template, data) => $"[{DateTime.Now}] {template}: {data}";
}

// Использование с частичным применением
var service = new ReportService();
var formatWithTemplate = service.FormatReport("Error"); // фиксируем шаблон
string errorMessage = formatWithTemplate("Connection failed"); 
// Результат: "[05.04.2025 14:30:00] Error: Connection failed"

3. Сопоставление с образцом (Pattern Matching) на стероидах

C# 7+ и .NET Core 3+ предоставили мощный механизм сопоставления с образцом. На продвинутом уровне его можно комбинировать с discriminated unions (алгебраическими типами данных), которые эмулируются через иерархию классов и рекордов.

Рассмотрим пример обработки результата HTTP-запроса:

public abstract record HttpResult<T>;
public record Success<T>(T Data) : HttpResult<T>;
public record NotFound(string Message) : HttpResult<object>;
public record Error(string Code, string Description) : HttpResult<object>;

public string HandleResult(HttpResult<object> result) => result switch
{
    Success<object> { Data: var d } => $"Успех: {d}",
    NotFound { Message: var msg } => $"Не найдено: {msg}",
    Error { Code: "500", Description: var desc } => $"Серверная ошибка: {desc}",
    Error _ => "Неизвестная ошибка",
    _ => "Необработанный результат"
};

Компилятор C# проверяет полноту сопоставления (exhaustiveness), что предотвращает ошибки времени выполнения. Это намного безопаснее, чем цепочки if-else или switch по строкам.

4. Композиция функций через LINQ и кастомные операторы

Функциональный стиль поощряет композицию — создание новых функций путём объединения существующих. В C# для этого удобно использовать LINQ и методы расширения.

Продвинутый приём — композиция с помощью конвейера (pipeline). Создадим generic-функцию для композиции:

public static class FuncExtensions
{
    // Компози                

Блоги

Рекомендаций по книгам