Всегда ли значимые типы в C# хранятся в стеке
Что проверяют
Знаете ли вы, что «значимые типы живут в стеке» это упрощение: место хранения определяется не типом, а тем, где объявлена переменная и сколько она живёт. Следом часто спрашивают про упаковку (boxing) или про ref struct и Span<T>.
Как отвечать
Короткий ответ: нет. Значимый тип хранится там, где хранится его контейнер:
- локальная переменная и параметр: обычно в стеке или вообще в регистре, решает JIT;
- поле класса: внутри объекта в управляемой куче, вместе с ним;
- элемент массива:
int[]это объект в куче, элементы лежат в нём подряд без отдельных ссылок; - упаковка: при приведении к
objectили к интерфейсу (IComparable,IFormattable) значение копируется в новый объект в куче; - захват в замыкание: локальная
int, которую использует лямбда, становится полем сгенерированного класса и уезжает в кучу; asyncи итераторы: локальные переменные, которые переживаютawaitилиyield return, становятся полями машины состояний. У итератора это класс, уasync-метода структура, которая переезжает в кучу при первом незавершённомawait.
Обратная сторона: stackalloc и ref struct (например, Span<T>) гарантированно не попадают в кучу, компилятор запрещает их упаковку и хранение в полях классов.
Хорошо сослаться на мысль Эрика Липперта: стек это деталь реализации, а в языке важна семантика копирования. Значимый тип копируется при присваивании целиком, ссылочный передаёт ссылку на тот же объект.
Где спотыкаются
Отвечают «да, всегда в стеке» или «структуры в стеке, классы в куче». Это сразу показывает, что тема знакома по верхам.
Путают передачу параметра по значению с местом хранения. Копия при вызове метода говорит о семантике, а не о том, стек это или куча.
Не могут назвать, откуда упаковка берётся в реальном коде: вызов интерфейсного метода на структуре через переменную интерфейсного типа, параметры object вроде string.Format("{0}", 42), необобщённые коллекции. А это и есть практический смысл вопроса: лишние аллокации в горячем цикле.