Функциональное программирование в C#: от теории к практике
Функциональное программирование (ФП) — это парадигма, в которой вычисления рассматриваются как вычисление математических функций, избегая изменения состояния и изменяемых данных. Хотя C# изначально был объектно-ориентированным языком, начиная с версии 3.0 он активно впитывает функциональные возможности. Сегодня C# поддерживает такие концепции, как лямбда-выражения, LINQ, неизменяемые типы и даже pattern matching. В этой статье мы разберем ключевые принципы ФП и покажем, как их применять в реальных проектах на C#.
1. Чистые функции и отсутствие побочных эффектов
Чистая функция — это функция, которая:
- Всегда возвращает один и тот же результат для одних и тех же аргументов (ссылочная прозрачность).
- Не имеет побочных эффектов (не изменяет глобальное состояние, не пишет в файлы, не отправляет HTTP-запросы).
В C# чистая функция может выглядеть так:
public static int Add(int a, int b) => a + b;
А вот пример нечистой функции:
public static int AddAndLog(int a, int b)
{
Console.WriteLine($"Складываем {a} и {b}"); // побочный эффект
return a + b;
}
Преимущества чистых функций: их легко тестировать, отлаживать и параллелить.
2. Неизменяемость (Immutability)
В ФП данные по умолчанию неизменяемы. В C# для этого используются:
- Ключевое слово
readonlyдля полей. - Структуры
record(C# 9+), которые автоматически создают неизменяемые объекты. - Коллекции из пространства имен
System.Collections.Immutable.
Пример с record:
public record Person(string Name, int Age);
Если нужно изменить возраст, создается новый экземпляр:
Person person = new("Иван", 30);
Person olderPerson = person with { Age = 31 };
Неизменяемость предотвращает случайные мутации и упрощает многопоточность.
3. Функции высшего порядка и лямбда-выражения
Функции высшего порядка принимают другие функции как аргументы или возвращают их. В C# это реализуется через делегаты: Func<>, Action<>, Predicate<>.
Простой пример — метод Where из LINQ:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
Здесь n => n % 2 == 0 — это лямбда-выражение (анонимная функция).
Можно написать свою функцию высшего порядка:
public static List<T> Filter<T>(List<T> list, Func<T, bool> predicate)
{
var result = new List<T>();
foreach (var item in list)
if (predicate(item)) result.Add(item);
return result;
}
4. LINQ: мощный инструмент функционального стиля
LINQ (Language Integrated Query) — это, пожалуй, самый яркий пример функционального программирования в C#. Он позволяет обрабатывать коллекции декларативно, как в SQL.
Пример: получить имена пользователей старше 18 лет, отсортированные по возрасту:
var users = new List<User>
{
new("Анна", 22),
new("Борис", 17),
new("Виктор", 30)
};
var result = users
.Where(u => u.Age >= 18)
.OrderBy(u => u.Age)
.Select(u => u.Name)
.ToList();
LINQ поддерживает отложенное выполнение (lazy evaluation), что экономит ресурсы.
5. Монады и Maybe-тип в C#
Монады — это абстракция для цепочек вычислений с контекстом (например, обработка null или асинхронность). В C# нет встроенной монады Maybe, но её легко реализовать:
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(value, true);
public static Maybe<T> None() => new(default!, false);
public Maybe<TResult> Bind<TResult>(Func<T, Maybe<TResult>> func) =>
HasValue ? func(_value) : Maybe<TResult>.None();
}
Использование:
Maybe<int> GetUserAge(string userId) { /* ... */ }
Maybe<string> GetDiscount(int age) { /* ... */ }
var discount = GetUserAge("123")
.Bind(age => GetDiscount(age));
Такой подход заменяет бесконечные проверки на null и делает код более предсказуемым.
6. Pattern Matching (сопоставление с образцом)
Начиная с C# 7, появился мощный механизм сопоставления с образцом. Он идеально вписывается в функциональный стиль:
public static string Describe(object obj) => obj switch
{
int i when i > 0 => "Положительное целое",
int i when i < 0 => "Отрицательное целое",
string s => $"Строка длиной {s.Length}",
null => "null",
_ => "Другой тип"
};
Это избавляет от громоздких конструкций if-else и делает код более читаемым.
Заключение
Функциональное программирование в C# — это не просто модный тренд, а мощный инструмент для написания надёжного, тестируемого и параллельного кода. Даже если вы не планируете полностью переходить на ФП, внедрение отдельных принципов (чистые функции, неизменяемость, LINQ) значительно улучшит качество ваших проектов. Начните с малого: замените циклы на LINQ, использу