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

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

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

Начать курс

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

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

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

1. Чистые функции и отсутствие побочных эффектов

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

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

В C# чистая функция может выглядеть так:

// Чистая функция
public int AddPure(int a, int b)
{
    return a + b;
}

// НЕ чистая функция (побочный эффект — вывод на консоль и изменение глобальной переменной)
private int _state = 0;
public int AddImpure(int a, int b)
{
    _state++;
    Console.WriteLine("Вызов функции");
    return a + b + _state;
}

Преимущества чистых функций очевидны: их легко тестировать, они предсказуемы и их можно безопасно вызывать из параллельных потоков. Старайтесь выносить всю логику, не связанную с I/O, в чистые функции.

2. Неизменяемость данных (Immutability)

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

  • record (начиная с C# 9) — ссылочный тип с value-based равенством и неизменяемостью по умолчанию.
  • struct — для небольших неизменяемых структур (например, координаты, деньги).
  • IEnumerable<T> — для неизменяемых коллекций через LINQ.

Пример с record:

public record Person(string Name, int Age);

// Использование with-выражения для создания копии с изменениями
var original = new Person("Алексей", 30);
var updated = original with { Age = 31 };

Console.WriteLine(original); // Person { Name = Алексей, Age = 30 }
Console.WriteLine(updated);  // Person { Name = Алексей, Age = 31 }

Такой подход исключает случайное изменение данных в одном месте, которое влияет на другие части программы.

3. LINQ — функциональные запросы к данным

LINQ (Language Integrated Query) — это, пожалуй, самый яркий пример функционального подхода в C#. Он позволяет обрабатывать коллекции в декларативном стиле, используя цепочки методов.

Ключевые функциональные методы LINQ:

  • Select — трансформация каждого элемента (аналог map).
  • Where — фильтрация (аналог filter).
  • Aggregate — свёртка (аналог reduce).
  • SelectMany — развёртывание вложенных коллекций (flatMap).

Пример: найти сумму квадратов чётных чисел.

int[] numbers = { 1, 2, 3, 4, 5, 6 };

// Императивный стиль
int sumImperative = 0;
foreach (var num in numbers)
{
    if (num % 2 == 0)
    {
        sumImperative += num * num;
    }
}

// Функциональный стиль с LINQ
int sumFunctional = numbers
    .Where(n => n % 2 == 0)   // фильтрация
    .Select(n => n * n)      // трансформация
    .Sum();                    // агрегация

Console.WriteLine(sumFunctional); // 56 (4 + 16 + 36)

Обратите внимание: все методы LINQ возвращают новый IEnumerable и не изменяют исходную коллекцию. Это и есть принцип неизменяемости в действии.

4. Лямбда-выражения, делегаты и замыкания

Лямбда-выражения — это анонимные функции, которые можно передавать как данные. В C# они тесно связаны с делегатами Func<> и Action<>.

Базовый синтаксис:

Func<int, int, int> multiply = (x, y) => x * y;
Console.WriteLine(multiply(5, 3)); // 15

Замыкание (closure) — это лямбда-функция, которая "захватывает" переменные из внешнего контекста. Это мощный, но опасный инструмент, так как он может привести к неожиданным эффектам.

public Func<int> CreateCounter()
{
    int count = 0;
    return () => ++count; // замыкание: захватывает переменную count
}

var counter = CreateCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2

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

Заключение

Функциональное программирование в C# — это не замена ООП, а мощное дополнение к нему. Используя чистые функции, неизменяемые записи (record), LINQ и лямбда-выражения, вы получаете:

  • Более предсказуемый и тестируемый код.
  • Упрощённую параллельную обработку данных.
  • Лаконичность и выразительность (особенно при работе с коллекциями).

Начните с малого: замените циклы на LINQ, используйте record для моделей данных и старайтесь писать чистые функции везде, где это возможно. Со временем вы почувствуете,

Блоги

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