JavaRush /Курсы /C# SELF /Практическое применение ФП в C#

Практическое применение ФП в C#

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

1. Введение

Переходя от теории к практике, логично задаться вопросом: «А зачем мне, разработчику на .NET и C#, все эти приёмы функционального программирования?»
Действительно, C# — не чисто функциональный язык вроде F# или Haskell. Но начиная с версии 3.0 и вплоть до C# 14, он обзавёлся множеством инструментов ФП, которые могут серьёзно поднять качество и выразительность кода.

Вот где они особенно хорошо работают:

  • Работа с коллекциямиLINQ, Map/Reduce, фильтрация, агрегация, сортировка и прочая «магия» с данными.
  • Чистые функции — меньше багов из-за состояния и побочных эффектов, проще отлаживать.
  • Функции высшего порядка — универсальные, переиспользуемые компоненты, с которыми приятно работать.
  • Неизменяемость в многопоточности — один из главных залогов безопасного кода в параллельных и асинхронных сценариях.
  • Композиция функций — делает сложную бизнес-логику лаконичной, читаемой и удобной для тестирования.

Таблица: Сравнение ООП-подхода и ФП-подхода в C#

Задача Императивно (ООП/старая школа) Функционально (ФП)
Фильтрация списка
foreach + if + Add
.Where(predicate)
Преобразование списка
foreach + вычисления + Add
.Select(lambda)
Поиск по критерию
foreach + if/return
.FirstOrDefault(predicate)
Агрегирование
цикл с переменной-счетчиком
.Aggregate(seed, func)
Кеширование
ручной словарь + проверки
функция с closure

2. LINQ: самое функциональное в C#

Если вам показалось, что “функциональное программирование” — это про списки, фильтры и всякие .Where, .Select, .Aggregate, — поздравляю: это действительно так! LINQ — квинтэссенция ФП в C#.

Давайте вспомним, как LINQ устроен

LINQ оперирует коллекциями с помощью цепочек методов, принимающих функции-параметры (например, лямбды). Например:

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Получим только чётные числа и удвоим их
var result = numbers
    .Where(x => x % 2 == 0)
    .Select(x => x * 2);

foreach (var number in result)
    Console.WriteLine(number);

Что тут происходит?

  • .Where — функция высшего порядка: принимает функцию (x => x % 2 == 0) и возвращает другую коллекцию.
  • .Select — тоже принимает функцию (x => x * 2).
  • Мы не изменяем исходную коллекцию, а получаем новый результат.

Этот стиль удобно читать и расширять (можно прицепить ещё .OrderBy, .Take, .Distinct и т.д.).

Обратите внимание! Лямбда-выражения — удобный способ создать «делегат на лету». LINQ был бы невозможен без поддержки ФП в C#.

Схема: Функциональная обработка коллекции


Коллекция --> Where(x => bool) --> Select(x => y) --> Новый результат

3. Композиция функций и конвейер обработки данных

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

Пример: цепочка обработки строки

Стиль с изменением состояния (ООП):

string s = "   hello world   ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!

Более «функционально» — как конвейер функций:

Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";

// Композиция функций — последовательно применяем их
Func<string, string> pipeline = x => addBang(upper(trim(x)));

Console.WriteLine(pipeline("   hello world   ")); // HELLO WORLD!

Простейший комбинатор композиции:

Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
    x => g(f(x));

// А теперь pipeline через Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);

Console.WriteLine(pipeline2("   hello again    ")); // HELLO AGAIN!

4. Работа с неизменяемостью: защита от багов

Иммутабельность — основной кирпич ФП. Мы не изменяем структуру данных, а возвращаем новую. Это особенно важно в многопоточных приложениях.

Пример: «неправильно» (мутабельно)

List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;

Пример: «правильно» (функционально)

var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();

В современном C# есть коллекции ImmutableList<T> и другие типы из пространства имён System.Collections.Immutable:

using System.Collections.Immutable;

var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // Возвращает новый список!

5. Функции высшего порядка в реальной жизни

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

Пример: Универсальный фильтр для пользователей

class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var users = new List<User>
{
    new User { Name = "Вася", Age = 26 },
    new User { Name = "Катя", Age = 17 },
    new User { Name = "Лёша", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
    return source.Where(u => predicate(u)).ToList();
}

// Использование:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);

6. Pattern matching и switch-выражения

Современный C# активно использует сопоставление с образцом: switch-выражения часто заменяют тяжелые цепочки if.

object value = 123;

string description = value switch
{
    int i when i > 100 => "Большое число",
    string s when s.Length > 3 => "Строка длинная",
    null => "Пустое значение",
    _ => "Неизвестно"
};

Console.WriteLine(description); // Большое число

7. Мемоизация: кеширование результатов функций

Мемоизация — кеширование результата функции для одинаковых аргументов. В C# её легко реализовать самостоятельно.

Func<int, int> SlowFib = null; // Рекурсивная функция Фибоначчи

var cache = new Dictionary<int, int>();

SlowFib = n =>
{
    if (cache.ContainsKey(n))
        return cache[n];
    if (n <= 1)
        cache[n] = n;
    else
        cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
    return cache[n];
};

Console.WriteLine(SlowFib(40)); // Молниеносно!

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

Частичное применение — фиксация части аргументов функции. В C# это удобно делать лямбдами.

Func<int, int, int> add = (a, b) => a + b;

// Фиксируем первый аргумент
Func<int, int> add10 = b => add(10, b);

Console.WriteLine(add10(5));   // 15
Console.WriteLine(add10(100)); // 110

9. Декларативный стиль с функциями

Императивно:

var result = new List<int>();
foreach (var n in numbers)
{
    if (n > 0)
        result.Add(n * n);
}

Декларативно:

var result = numbers
    .Where(n => n > 0)
    .Select(n => n * n)
    .ToList();

10. Практическая задача

Реализуем модуль фильтрации задач в «менеджере задач для студентов».

Модель:

class StudentTask
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
    public int Priority { get; set; }
}

Исходные данные:

var tasks = new List<StudentTask>
{
    new StudentTask { Title = "Сделать домашку", IsCompleted = false, Priority = 2 },
    new StudentTask { Title = "Попить кофе", IsCompleted = true, Priority = 3 },
    new StudentTask { Title = "Посмотреть лекцию", IsCompleted = false, Priority = 1 }
};

Универсальный фильтр:

List<StudentTask> FilterTasks(
    List<StudentTask> all,
    Predicate<StudentTask> predicate)
{
    return all.Where(t => predicate(t)).ToList();
}

// Поиск незавершённых задач с приоритетом > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);

// Выводим результат
foreach (var task in importantTasks)
    Console.WriteLine(task.Title);

Комбинаторы предикатов:

Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;

// Объединяем несколько критериев, вариант 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));

// Вариант 2: функция-комбинатор двух предикатов
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);

var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));

11. Особенности и типичные ошибки при применении функционального подхода в C#

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

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

В-третьих, будьте аккуратны с захватом переменных внешней области (closure capture), особенно в асинхронном и многопоточном коде. Переменная, значение которой меняется после передачи в лямбду (например, в LINQ), может приводить к неочевидным багам.

Наконец, избыточный ФП-стиль может усложнить поддержку кода для команды, не привыкшей к нему. Используйте ФП там, где это реально упрощает решение, а не ради «красоты».

2
Задача
C# SELF, 51 уровень, 4 лекция
Недоступна
Обработка строк с композицией функций
Обработка строк с композицией функций
1
Опрос
Функциональное программирование, 51 уровень, 4 лекция
Недоступен
Функциональное программирование
Введение в функциональное программирование
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Кирилл Уровень 63
21 января 2026
получается задача из прошлой лекции это пример Currying (Каррирования)