Что происходит при переполнении стека, например при бесконечной рекурсии?
Что проверяют
Понимаете ли вы, чем стек отличается от кучи и почему его переполнение в .NET не обычное исключение. Заодно смотрят на отношение к рекурсии: кандидат, который обещает поймать StackOverflowException в catch, выдаёт, что таких аварий не разбирал.
Как отвечать
Каждый вызов кладёт на стек кадр: адрес возврата, аргументы, локальные значимые переменные, сохранённые регистры. Стек потока фиксированного размера, задаётся при создании потока, у главного берётся из заголовка исполняемого файла и измеряется мегабайтами. Рекурсия без условия выхода упирается в границу, среда выполнения ловит выход за сторожевую страницу и завершает процесс.
Дальше главное: начиная с .NET Framework 2.0 StackOverflowException не перехватывается. Не сработают ни try/catch, ни finally, ни глобальный обработчик, процесс убивается сразу, потому что продолжать на разрушенном стеке небезопасно. Значит, защита только превентивная:
- условие выхода и явное ограничение глубины;
- переписать на цикл со своим стеком или очередью, для обхода деревьев это обычная практика;
RuntimeHelpers.EnsureSufficientExecutionStackтам, где глубина зависит от входных данных, например в разборе выражений;- поток с увеличенным размером стека как крайняя мера, а не как решение.
Уточните, что C# не гарантирует оптимизацию хвостового вызова: компилятор её не делает, так что фраза «хвостовая рекурсия безопасна» здесь неверна.
Где спотыкаются
Первое: обещают обернуть рекурсию в try/catch и показать пользователю аккуратное сообщение. Так не выйдет, и сам ответ читается как отсутствие опыта с боевыми падениями.
Второе: считают, что переполнение бывает только от явной рекурсии. Взаимный вызов двух методов, свойство, которое присваивает само себя, рекурсивный ToString или Equals по циклической ссылке, глубокая цепочка сериализации: всё это тот же стек. Крупный stackalloc в цикле съедает его ещё быстрее.
Третье: путают с OutOfMemoryException. Первое про фиксированный стек потока, второе про кучу, и добавление памяти машине от первого не спасает. Рекурсия в async методе вообще живёт в куче, поэтому вместо мгновенной аварии даёт рост потребления памяти.