Какие есть способы вызвать исключение StackOverflowException в C#?

«Какие есть способы вызвать исключение StackOverflowException в C#?» — вопрос из категории Управление памятью, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

StackOverflowException возникает при переполнении стека вызовов — области памяти с фиксированным размером (обычно 1-4 МБ в Windows), где хранятся локальные переменные и информация о вызовах методов. Вот основные причины:

1. Бесконечная или слишком глубокая рекурсия

Классическая причина. Каждый рекурсивный вызов помещает новый фрейм в стек.

// Пример 1: Бесконечная рекурсия (отсутствует базовый случай)
void InfiniteRecursion()
{
    InfiniteRecursion(); // StackOverflowException гарантирован
}

// Пример 2: Слишком глубокая рекурсия с базовым случаем
int Factorial(int n)
{
    if (n <= 1) return 1; // Базовый случай есть
    return n * Factorial(n - 1); // При очень большом n стек переполнится
}
// Вызов Factorial(100000) почти наверняка вызовет исключение.

2. Взаимная рекурсия (косвенная рекурсия)

Два или более метода вызывают друг друга по кругу.

void MethodA() { MethodB(); }
void MethodB() { MethodA(); } // Вызов MethodA() -> MethodB() -> MethodA()...

3. Большие структуры, размещенные на стеке

Локальные переменные-значения (структуры) размещаются в стеке. Очень большая структура может переполнить его сразу.

// Опасная большая структура
public struct MassiveStruct
{
    public fixed byte Data[1_000_000]; // 1 МБ данных (unsafe контекст)
}

void CauseOverflow()
{
    MassiveStruct big; // Попытка разместить 1 МБ на стеке вызовет переполнение
}

В безопасном коде большие массивы внутри структур будут размещаться в куче, но сама переменная структуры и ссылка на массив — в стеке.

4. Чрезмерно длинные цепочки вызовов методов

Глубокая вложенность вызовов без рекурсии также может исчерпать стек, особенно в event-driven или callback-архитектурах.

void Level1() { Level2(); }
void Level2() { Level3(); }
// ... много уровней ...
void Level9999() { Console.WriteLine("Deep"); }
// Вызов Level1() при большом количестве уровней приведет к переполнению.

Как предотвратить и обработать?

  1. Замените рекурсию на итерацию. Используйте циклы или структуру данных Stack<T> для явного управления состоянием в куче.
    // Итеративный факториал вместо рекурсивного
    int FactorialIterative(int n)
    {
        int result = 1;
        for (int i = 2; i <= n; i++)
            result *= i;
        return result;
    }
  2. Увеличьте размер стека (не рекомендуется). Можно задать больший размер стека для потока через конструктор Thread или в настройках компоновщика для основного потока. Это лишь откладывает проблему.
  3. Используйте хвостовую рекурсию (с осторожностью). В .NET JIT-компилятор ограниченно применяет оптимизацию хвостовых вызовов. Не стоит на нее полагаться для предотвращения StackOverflowException.

Критично: StackOverflowException в .NET обычно неперехватываемое (uncatchable) начиная с .NET 2.0. Процесс будет аварийно завершен. Поэтому основная стратегия — предотвращение через правильный дизайн алгоритмов.