Современный C#
C# развивается быстро: с версии 9 (2020) по версию 14 (ноябрь 2025, в составе .NET 10 LTS) язык обзавёлся целым слоем возможностей, которые радикально сокращают шаблонный код. Почти все они объединены одним свойством — это работа компилятора, а не среды выполнения. Коллекционное выражение, target-typed new(), field, extension-блок, nullable-аннотация: каждая из этих конструкций разворачивается в обычный IL, который вы могли бы написать руками. Именно поэтому собеседование по «современному C#» — это почти всегда вопрос «во что это компилируется?», а не «как это пишется?».
Отсюда и главные ловушки. Nullable reference types не добавляют ни одной проверки в рантайме — они только выдают предупреждения, и чистая сборка ничего не гарантирует. Коллекционное выражение берёт тип из цели, а не из элементов. is null и == null — разные механизмы: первый компилируется в сравнение ссылок, второй может вызвать перегруженный оператор. field — контекстное ключевое слово, и это единственное за долгое время изменение, которое может сломать существующий код. А ?. слева от = в C# 14 не просто «пропускает присваивание» — он ещё и не вычисляет правую часть. Разбор по слоям — ниже.
Карта темы
- Top-level statements — точка входа без класса и без
Main, которые компилятор дописывает за вас. - Target-typed new() — тип берётся из цели присваивания, а не из аргументов конструктора.
- Сырые строковые литералы —
"""без экранирования и с отсечением отступа по закрывающему разделителю. - Коллекционные выражения —
[1, 2, 3]и spread.., из которых компилятор строит массив,List<T>илиSpan<T>по цели. - Свойства — авто-свойство со скрытым backing-полем против expression-bodied, пересчитываемого при каждом чтении.
- Ключевое слово field — доступ к синтезированному backing-полю прямо в аксессоре и то, почему это ломающее изменение.
- Nullable reference types —
string?противstring, анализ потока данных и полное отсутствие рантайм-проверок. - Константные паттерны и is null — почему
is nullникогда не вызовет пользовательскийoperator ==. - Null-conditional-присваивание —
?.и?[]слева от=в C# 14 и невычисляемая правая часть. - Extension-члены — блок
extension(...)в C# 14, который умеет объявлять свойства, статические члены и операторы.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что top-level statements можно писать в нескольких файлах | Ошибка компиляции — точка входа одна, и такой файл в проекте ровно один |
Думать, что target-typed new() выводит тип из аргументов конструктора | var x = new(); не компилируется: var не даёт цели, из которой можно вывести тип |
| Экранировать кавычки внутри сырого литерала | Обратный слэш в """ — обычный символ; экранирование только портит текст |
| Не знать, что отступ сырого литерала отсекается по закрывающему разделителю | Строка с меньшим отступом, чем у """, — ошибка компиляции |
Выводить тип [1, 2, 3] из элементов | Тип берётся из цели; без цели (var x = [1, 2, 3];) выражение не компилируется |
Ждать от ..other вложения коллекции | Spread вклеивает элементы, а не кладёт коллекцию внутрь как один элемент |
| Считать, что expression-bodied свойство хранит значение | Оно пересчитывает выражение при каждом чтении — backing-поля нет вовсе |
Считать string? синонимом Nullable<string> | Nullable<T> — обёртка только для значимых типов; string? — аннотация в метаданных |
| Ждать, что nullable-аннотации проверяются в рантайме | Не проверяются: IL не меняется, диагностика — предупреждения на этапе компиляции |
Считать x == null и x is null одним и тем же | == вызывает пользовательскую перегрузку, если она есть; is null — никогда |
Думать, что при customer?.Order = GetOrder() правая часть всё равно вычисляется | Она не вычисляется: null-получатель обрывает всё выражение, GetOrder() не вызывается |
Ждать, что customer?.Order++ теперь легален | Инкремент и декремент через ?. по-прежнему ошибка компиляции |
Ждать, что Span<T> неявно превратится обратно в T[] | Преобразования первоклассных span'ов односторонние — обратного пути нет |
| Пытаться добавить extension-блоком поле к чужому типу | Extension-члены не несут состояния — хранить его негде |
Значение для собеседований
«Современный C#» — любимый раздел интервьюера, потому что он мгновенно отделяет тех, кто читал release notes, от тех, кто понимает язык. Почти на каждый вопрос здесь есть правильный ответ вида «компилятор разворачивает это в …», и именно его ждут.
Что обычно проверяют:
- Что генерирует компилятор для top-level statements и почему такой файл в проекте один.
- Откуда target-typed
new()берёт тип и где его применить нельзя. - Как сырой литерал обходится без экранирования и что происходит с отступами.
- Во что компилируется
[1, 2, 3]для разных целевых типов и что делает... - Разницу авто-свойства и expression-bodied свойства на уровне сгенерированных членов.
- Что именно означает
fieldвнутри аксессора и почему это ломающее изменение. - Что nullable reference types дают на этапе компиляции и чего не дают в рантайме.
- Почему
is nullбезопаснее== null. - Что происходит при
customer?.Order = GetOrder(), когдаcustomer—null. - Что нового умеет
extension-блок C# 14 по сравнению с классическимthis-параметром.
Типичный неверный ответ: «Nullable reference types защищают от NullReferenceException». Не защищают — они не добавляют ни единой проверки в рантайме. Это статический анализ, который выдаёт предупреждения; null по-прежнему приходит из неаннотированных библиотек, из десериализации, из default! и из любого места, где кто-то поставил !. Правильный ответ формулирует границу: компилятор — да, среда выполнения — нет.