Avatar ·

В чем разница между декларативным и императивным программированием в C#?

Всем привет! Я начинающий C# разработчик, и меня постоянно путают термины "императивное" и "декларативное" программирование. Читал разные статьи, но до конца не понял разницу на практике. Например, я пишу такой код для фильтрации списка чисел:
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List<int> evenNumbers = new List<int>();

for (int i = 0; i < numbers.Count; i++)
{
    if (numbers[i] % 2 == 0)
    {
        evenNumbers.Add(numbers[i]);
    }
}
Это же императивный подход, да? Я явно описываю КАК делать: создаю цикл, проверяю условие, добавляю в список. Но потом я вижу другой способ:
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
Это уже декларативный? Я просто говорю ЧТО хочу получить (четные числа), а не как именно это делать. Но я не уверен до конца. В чем именно ключевое отличие? Когда какой подход использовать? Есть ли ситуации, где один явно лучше другого? Буду благодарен за подробное объяснение с примерами на C#! <<>>

Ключевое отличие: "КАК" против "ЧТО"

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

  • Императивное программирование — вы описываете последовательность шагов, которые компьютер должен выполнить для достижения результата. Вы контролируете каждую деталь: как создать список, как перебрать элементы, как проверить условие, как добавить результат.
  • Декларативное программирование — вы описываете желаемый результат, а детали реализации (как именно это сделать) оставляете языку или библиотеке. Вы говорите "что", а не "как".

Ваш пример с фильтрацией — это идеальная иллюстрация

Первый код (с циклом for) — это чистый императивный стиль. Вы вручную управляете:

  • Созданием пустого списка evenNumbers
  • Индексом i и его инкрементом
  • Проверкой каждого элемента
  • Добавлением в новый список

Второй код (с LINQ Where) — это декларативный подход. Вы просто объявляете: "дай мне числа, где n % 2 == 0". Как именно LINQ будет фильтровать (через цикл, через итератор, с оптимизациями) — это уже забота .NET Runtime. Вы делегируете выполнение.

Другие примеры на C#

1. Сортировка списка

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

List<int> numbers = new List<int>() { 3, 1, 4, 1, 5, 9 };
for (int i = 0; i < numbers.Count - 1; i++)
{
    for (int j = 0; j < numbers.Count - i - 1; j++)
    {
        if (numbers[j] > numbers[j + 1])
        {
            int temp = numbers[j];
            numbers[j] = numbers[j + 1];
            numbers[j + 1] = temp;
        }
    }
}

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

var sorted = numbers.OrderBy(n => n).ToList();

2. Сумма элементов

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

int sum = 0;
foreach (int num in numbers)
{
    sum += num;
}

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

int sum = numbers.Sum();

Когда что использовать?

  • Императивный подход лучше, когда:
    • Вам нужен максимальный контроль над производительностью (например, ручная оптимизация циклов)
    • Вы работаете с очень специфичной логикой, которую нельзя выразить стандартными методами
    • Вы пишете низкоуровневый код (работа с указателями, unsafe код)
  • Декларативный подход (LINQ, SQL, XAML) лучше, когда:
    • Код должен быть читаемым и понятным другим разработчикам
    • Вы хотите избежать ошибок, связанных с ручным управлением индексами и состояниями
    • Вы работаете с коллекциями, базами данных, XML — там, где есть готовые высокоуровневые операции

Важное замечание: гибридный подход

В реальных проектах вы почти всегда будете использовать оба подхода. Например, метод LINQ Where внутри себя реализован императивно (с помощью циклов и итераторов). А в вашем императивном коде вы можете использовать декларативные конструкции (например, foreach вместо for).

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

<<>>

Дополнение: императивное vs декларативное с точки зрения состояния

Еще один важный аспект — это управление состоянием (state). В императивном программировании вы часто изменяете состояние объектов (мутируете переменные, списки). В декларативном — вы создаете новые объекты на основе существующих, не изменяя исходные.

Вернемся к вашему примеру. В императивном коде вы создаете evenNumbers и заполняете его. Исходный список numbers не меняется. Но сама операция — мутация нового списка. В декларативном LINQ-варианте Where не изменяет исходный список и не создает промежуточные списки вручную — он возвращает IEnumerable, который лениво вычисляется.

Этот принцип (неизменяемость, immutability) — ключевой в функциональном программировании, которое является подвидом декларативного. В C# вы все чаще будете встречать рекомендации использовать IEnumerable и LINQ вместо явных циклов, особенно в асинхронном и многопоточном коде, где мутация состояния приводит к багам.

Совет: начните с замены простых циклов на LINQ-выражения (Where, Select, Aggregate). Это сделает ваш код более читаемым и менее подверженным ошибкам. А когда понадобится тонкая настройка производительности — вернитесь к императивному стилю.