CLR и среда выполнения
Компилятор C# не создаёт машинный код. Он создаёт IL (Intermediate Language) и метаданные, упакованные в сборку, а исполняет всё это CLR (Common Language Runtime) — управляемая среда, которая JIT-компилирует IL под конкретный CPU, размещает объекты в управляемой куче, собирает мусор, проверяет безопасность типов и открывает метаданные рефлексии. Понимать CLR — значит понимать, что происходит между «я написал C#» и «CPU выполняет инструкции».
Сквозная идея темы: временем жизни объектов и трансляцией кода владеет среда, а не вы. Объекты освобождает недетерминированный GC; нативный код появляется лениво через JIT; идентичность и версионирование живут в манифесте сборки и строгом имени; а метаданные настолько богаты, что поверх них работают рефлексия, LINQ-провайдеры и сериализаторы — над типами, неизвестными на этапе компиляции. Карта ниже идёт от модели исполнения к сервисам, построенным на метаданных.
Карта темы
- Управляемый код — что значит «управляемый»: исполнение под CLR со сборкой мусора, безопасностью типов и JIT вместо прямого запуска на ОС.
- IL и JIT — путь C# → IL → нативный код: компилятор даёт байт-код, JIT переводит его по методу при первом вызове и кэширует.
- Сборки —
.dll/.exeкак единица развёртывания: манифест, метаданные типов, встроенный IL и ресурсы. - Строгие имена — глобально уникальная идентичность через публичный ключ и подпись; вход в GAC без коллизий версий.
- Сборка мусора — поколения 0/1/2, отслеживание достижимости и mark-and-compact вместо ручного освобождения.
- Рефлексия — осмотр и вызов метаданных типов во время выполнения через
System.Reflection. - LINQ — единые операторы запросов над
IEnumerable/IQueryableс отложенным выполнением. - Сериализация — граф объектов ↔ JSON/XML/бинарь для хранения и передачи через границы процесса.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что C# компилируется сразу в нативный код | Пропущен этап IL; на деле нативный код рождает JIT при первом вызове метода |
| Ждать детерминированного освобождения по выходу из области видимости | GC собирает недетерминированно; для ресурсов нужен IDisposable/using, а не надежда на финализатор |
| Верить, что .NET использует подсчёт ссылок | Циклические ссылки «утекали» бы; на деле GC отслеживает достижимость от корней и собирает циклы |
Звать GC.Collect() руками ради «оптимизации» | Обычно бьёт по производительности: ломает поколенческую эвристику и провоцирует полные сборки |
| Путать сборку с пространством имён | Пространство имён — лишь имена; сборка — физическая единица развёртывания с манифестом и версией |
| Считать, что строгое имя шифрует сборку | Оно подписывает ради идентичности и целостности, а не конфиденциальности; IL остаётся читаемым |
| Думать, что LINQ выполняется жадно | Большинство запросов отложены; повторное перечисление прогоняет конвейер заново над изменившимся источником |
| Звать рефлексию в горячем пути | Доступ по метаданным медленнее прямого вызова; кэшируйте MemberInfo или компилируйте делегат |
| Считать входной сериализованный payload безопасным | Десериализация недоверенных данных (особенно бинарь) может упасть или выполнить чужой код |
Значение для собеседований
CLR спрашивают на middle/senior-интервью по C#, и проверяют не заучивание фактов, а модель исполнения: кто владеет памятью, откуда берётся нативный код, где живёт идентичность типа. Кандидат, который объясняет GC.Collect() через «ломает поколенческую эвристику», сразу выделяется на фоне «ну, чистит память».
Что обычно проверяют:
- Управляемый код и путь C# → IL → нативный код через JIT (и почему первый вызов метода «холодный»).
- Как GC решает, что объект недостижим; поколения, продвижение и почему подсчёт ссылок тут ни при чём.
- Что несёт сборка (манифест, метаданные, IL) и чем идентичность строгого имени отличается от имени файла.
- Что рефлексия умеет не только читать метаданные, но и создавать экземпляры и вызывать члены — с заметной ценой.
- Отложенное выполнение LINQ и разница
IEnumerableпротивIQueryable.
Типичный неверный ответ: «GC.Collect() нужно звать, чтобы освободить память». Это открывает разговор о том, что GC работает по своему расписанию, ручной вызов обычно вредит, а для своевременного освобождения ресурсов существует IDisposable/using, а не финализатор.