Можно ли гарантировать, что value type в C# не попадет на кучу
Что проверяют
Не держится ли у вас миф «значимые типы живут на стеке». Интервьюеру важно услышать, что место хранения определяется не типом, а тем, где живёт переменная, и что гарантию даёт только компилятор через ref struct. Следом спрашивают про Span<T> и stackalloc.
Как отвечать
Для обычной struct гарантии нет. Значение хранится там же, где его контейнер, и попадает в кучу в таких случаях:
- поле класса или элемент массива:
int[]лежит в куче целиком, вместе с элементами; - упаковка: приведение к
objectили к интерфейсу, вызовEqualsиGetHashCodeбез переопределения, необобщённые коллекции вродеArrayList; - замыкание: локальная переменная, захваченная лямбдой, переезжает в поле скрытого класса;
- async и итераторы: локальные переменные становятся полями машины состояний, которая уходит в кучу, если
awaitне завершился синхронно.
Гарантию даёт ref struct (с C# 7.2). Компилятор запрещает его упаковку, использование полем класса и элементом массива, захват в лямбду и хранение через границу await или yield. Поэтому Span<T> и ReadOnlySpan<T> сделаны ref struct, а Span<byte> buf = stackalloc byte[256]; даёт буфер на стеке без аллокаций. Размер такого буфера держат небольшим, иначе можно получить StackOverflowException.
Полезно добавить, что при вызове метода интерфейса через обобщённое ограничение where T : IComparable<T> упаковки нет, JIT генерирует отдельный код под каждый значимый тип. А в свежих версиях .NET JIT умеет сам размещать на стеке часть объектов, которые не покидают метод, но это оптимизация, а не гарантия.
Где спотыкаются
Отвечают «struct всегда на стеке» и тут же приводят пример с полем класса. Правильнее говорить о времени жизни переменной, а не о типе.
Не замечают скрытую упаковку: List<T> со struct-перечислителем, переданный как IEnumerable<T>, при foreach отдаёт упакованный перечислитель из кучи.
Считают, что readonly struct или модификатор in защищают от кучи. Они убирают лишние копии, а не аллокации.