Современный C#
Записи и value-равенство, сопоставление с образцом, init-only и required, nullable reference types, коллекционные выражения и возможности C# 14.
16 вопросов
JuniorТеорияЧастоЧто при #nullable enable отличает string от string??
Что при #nullable enable отличает string от string??
При включённом nullable string? объявляет ссылку, которая может держать null, а простой string компилятор считает никогда не null. Разыменование возможно-null значения без проверки даёт предупреждение (CS8602), как и присваивание null не-nullable ссылке.
Типичные ошибки
- ✗Думать, что
string?— этоNullable<string>; такая обёртка есть только для значимых типов - ✗Ждать жёстких ошибок компиляции; по умолчанию анализ выдаёт предупреждения
- ✗Считать, что не-nullable аннотация делает приход
nullв рантайме невозможным
Уточняющие вопросы
- →Что оператор подавления
!сообщает компилятору, и чего это вам стоит? - →Почему не-nullable свойство может держать
nullсразу после десериализации?
SeniorТеорияЧастоСколько раз в C# 14 вычисляется customer в customer?.Order += GetOrder()?
Сколько раз в C# 14 вычисляется customer в customer?.Order += GetOrder()?
Один раз. Null-условный оператор вычисляет получателя единожды и на null замыкается накоротко: не выполняется ничто — ни getter, ни setter, ни GetOrder(). Когда получатель не null, составное присваивание читает через get один раз и пишет через set один раз.
Типичные ошибки
- ✗Думать, что составное присваивание вычисляет получателя заново для чтения и ещё раз для записи
- ✗Считать, что правая часть выполняется до проверки на null, а результат просто отбрасывается
- ✗Забывать, что при null-получателе оба аксессора пропускаются полностью
Уточняющие вопросы
- →Почему язык не может поднять
customer?.Order++так же, как поднимает+=? - →Как переписать это вручную, получив то же короткое замыкание без
?.?
JuniorТеорияИногдаЧем автосвойство отличается от свойства с телом-выражением?
Чем автосвойство отличается от свойства с телом-выражением?
{ get; set; } — автосвойство: компилятор создаёт скрытое поле-хранилище, а аксессоры пишут в него и читают из него. Свойство с телом-выражением => expr объявляет только get, поля не создаёт и вычисляет выражение заново при каждом чтении. Обе формы компилируются в методы-аксессоры.
Типичные ошибки
- ✗Думать, что свойство с телом-выражением хранит значение, а не пересчитывает его
- ✗Считать, что автосвойство компилируется в обычное поле без аксессоров
- ✗Ожидать, что сгенерированное поле-хранилище автосвойства можно назвать в коде
Уточняющие вопросы
- →Почему в коде нельзя обратиться по имени к сгенерированному полю-хранилищу?
- →Что меняет для вызывающих добавление
private setк автосвойству?
JuniorТеорияИногдаЧем константный шаблон x is null отличается от записи x == null?
Чем константный шаблон x is null отличается от записи x == null?
is null — константный шаблон: компилятор сводит его к прямому сравнению ссылок и никогда не вызывает пользовательский operator ==. x == null вызывает эту перегрузку, если тип её объявил, поэтому может ответить иначе или даже бросить исключение. is not null — отрицающая форма.
Типичные ошибки
- ✗Считать, что
== nullвсегда сводится к сравнению ссылок, забыв про перегрузки оператора - ✗Думать, что
is nullспособен дойти до пользовательского оператора равенства - ✗Писать
!(x is null)вместо формыis not null
Уточняющие вопросы
- →Как перегруженный
operator ==может заставитьx == nullуйти в бесконечную рекурсию? - →Что проверяет
x is nullдляNullable<T>, например дляint??
JuniorТеорияИногдаКакую проблему решают raw string literals, и как они обходятся с отступами?
Какую проблему решают raw string literals, и как они обходятся с отступами?
Raw string literal ограничен тремя или более двойными кавычками и не требует экранирования: кавычки, слэши и скобки остаются буквальными, поэтому JSON, регулярка и SQL читаются как есть. В многострочном литерале отступ закрывающего ограничителя срезается с каждой строки.
Типичные ошибки
- ✗Думать, что вложенные кавычки внутри raw-литерала всё ещё надо экранировать или удваивать
- ✗Не знать, что срезается именно отступ закрывающего ограничителя, а не произвольный
- ✗Считать, что ограничитель — ровно три кавычки, а не три и более
Уточняющие вопросы
- →Как вложить подряд три двойные кавычки внутрь raw string literal?
- →Что меняется, если добавить перед raw string literal один или два знака
$?
JuniorТеорияИногдаОткуда target-typed new() берёт тип, и где его нельзя применить?
Откуда target-typed new() берёт тип, и где его нельзя применить?
Target-typed new() берёт тип из цели присваивания, а не из аргументов, поэтому его можно опустить везде, где тип цели известен: поле, локальная переменная, параметр, возврат. Если выводить не из чего — var x = new(); — это ошибка компиляции.
Типичные ошибки
- ✗Думать, что тип выводится из аргументов конструктора, а не из цели присваивания
- ✗Писать
var x = new();—varне даёт цели, из которой можно вывести тип - ✗Полагать, что подстановка происходит в рантайме; её делает компилятор
Уточняющие вопросы
- →Как ведёт себя target-typed
new(), когда у цели есть две подходящие перегрузки? - →Когда опускание имени типа в
new()вредит читаемости, а не помогает?
JuniorТеорияИногдаЧто такое top-level statements, и что компилятор для них генерирует?
Что такое top-level statements, и что компилятор для них генерирует?
Начиная с C# 9 точку входа можно писать голыми операторами в начале одного файла, без класса и без метода Main. Компилятор сам генерирует вокруг них класс Program и его Main. Внутри доступны args и await, а использовать их может лишь один файл проекта.
Типичные ошибки
- ✗Думать, что несколько файлов одного проекта могут нести top-level statements
- ✗Считать, что
argsиawaitвнутри top-level statements недоступны - ✗Полагать, что класса
Programвовсе нет — компилятор всё равно его генерирует
Уточняющие вопросы
- →Как прочитать аргументы командной строки и вернуть код возврата из top-level statements?
- →Куда попадают локальные функции и директивы
usingв файле с top-level statements?
MiddleТеорияИногдаЧто в C# 14 происходит в customer?.Order = GetOrder(), если customer равен null?
Что в C# 14 происходит в customer?.Order = GetOrder(), если customer равен null?
C# 14 разрешает ?. и ?[] слева от присваивания и составного присваивания. Когда customer равен null, присваивание пропускается, а правая часть не вычисляется, поэтому GetOrder() не вызывается. customer?.Order++ остаётся ошибкой компиляции.
Типичные ошибки
- ✗Думать, что правая часть всё же выполняется при null-получателе — она не вычисляется
- ✗Ждать
NullReferenceExceptionвместо пропущенного присваивания - ✗Полагать, что
customer?.Order++теперь допустим — инкремент и декремент по-прежнему ошибки
Уточняющие вопросы
- →Сколько раз вычисляется получатель в
customer?.Order += GetOrder()? - →Почему инкремент и декремент остаются запрещёнными для null-условной цели?
MiddleТеорияИногдаЧто при #nullable enable проверяется на компиляции, а что — в рантайме?
Что при #nullable enable проверяется на компиляции, а что — в рантайме?
В рантайме не проверяется ничего: аннотации — метаданные, проверок на null не генерируется, IL не меняется. На компиляции анализ потока отслеживает null-состояние и выдаёт предупреждения, а не ошибки, поэтому null приходит из неаннотированных библиотек или через !.
Типичные ошибки
- ✗Ждать проверок на null в рантайме — их не генерируется
- ✗Считать чистую сборку доказательством того, что
NullReferenceExceptionневозможен - ✗Забывать, что неаннотированные зависимости целиком выпадают из анализа
Уточняющие вопросы
- →Как превратить nullable-предупреждения в ошибки сборки, и почему это решение уровня проекта?
- →Что оператор подавления
!на самом деле меняет в сгенерированном IL?
MiddleТеорияРедкоВо что компилируется коллекционное выражение [1, 2, 3], и что делает ..?
Во что компилируется коллекционное выражение [1, 2, 3], и что делает ..?
Коллекционное выражение типизируется по цели: [1, 2, 3] станет массивом, List<T>, Span<T> или типом с CollectionBuilder — тем, что требует цель, — а компилятор выберет способ построения. Спред ..other вставляет элементы коллекции на место.
Типичные ошибки
- ✗Думать, что тип выводится из элементов, а не из цели
- ✗Ждать, что
..otherвложит коллекцию, а не вставит её элементы - ✗Полагать, что всегда выделяется
List<T>, даже когда цель —Span<T>или массив
Уточняющие вопросы
- →Как компилятор строит коллекционное выражение, присвоенное в
ReadOnlySpan<T>? - →Что атрибут
CollectionBuilderпозволяет вашему типу коллекции делать с[...]?
MiddleТеорияРедкоЧто блок extension из C# 14 может объявить, а классический extension-метод — нет?
Что блок extension из C# 14 может объявить, а классический extension-метод — нет?
Блок extension(Receiver r) в статическом классе называет получателя один раз и может объявлять extension-свойства, статические члены и операторы — не только методы экземпляра. Классические методы с this работают, и состояние не добавляет ни одна форма.
Типичные ошибки
- ✗Думать, что блок
extensionможет добавить типу поля или состояние - ✗Считать, что классические extension-методы с параметром
thisперестают работать - ✗Полагать, что объявить можно только методы экземпляра — свойства, операторы и статические члены тоже
Уточняющие вопросы
- →Почему у extension-свойства не может быть собственного backing-поля?
- →Как компилятор разворачивает блок
extension, и что это значит для бинарной совместимости?
MiddleТеорияРедкоНа что ссылается ключевое слово field из C# 14 внутри аксессора свойства?
На что ссылается ключевое слово field из C# 14 внутри аксессора свойства?
field называет синтезированное компилятором backing-поле объявляемого свойства, поэтому один аксессор несёт логику, пока второй остаётся авто-реализованным. Работает в get, set и init. Слово контекстное, поэтому может затенить переменную с именем field.
Типичные ошибки
- ✗Думать, что
fieldсоздаёт новое поле, а не называет уже синтезированное - ✗Ждать, что его нельзя использовать в аксессорах
setилиinit - ✗Забывать, что слово контекстное, поэтому переменная с именем
fieldможет быть затенена
Уточняющие вопросы
- →Как обратиться к переменной с именем
fieldвнутри аксессора, использующего это слово? - →Что меняет
fieldдля свойства, у которого уже есть написанное вручную backing-поле?
SeniorТеорияРедкоПочему ключевое слово field из C# 14 — ломающее изменение, и как его экранировать?
Почему ключевое слово field из C# 14 — ломающее изменение, и как его экранировать?
Внутри аксессора свойства field теперь связывается с backing-полем от компилятора, поэтому существующий код с идентификатором field в области видимости может сменить смысл или перестать компилироваться. Экранируйте его как @field, а поле класса уточните через this.field.
Типичные ошибки
- ✗Думать, что
fieldзарезервировано везде, а не контекстно внутри аксессора свойства - ✗Полагать, что существующий код не задеть, тогда как идентификатор
fieldв области видимости и есть опасность - ✗Не знать, что
@field— илиthis.fieldдля поля класса — снимает неоднозначность
Уточняющие вопросы
- →С чем связывается
fieldв свойстве, у которого уже есть написанное вручную backing-поле? - →Почему выбрали контекстное слово, а не совершенно новую синтаксическую форму?
SeniorТеорияРедкоКакие конверсии добавляют first-class spans в C# 14, и какое направление невозможно?
Какие конверсии добавляют first-class spans в C# 14, и какое направление невозможно?
C# 14 делает конверсии span языковыми, поэтому они работают для получателей extension-методов и вывода типов. Они только расширяют — T[] → Span<T>/ReadOnlySpan<T>, Span<T> → ReadOnlySpan<T>, string → ReadOnlySpan<char> — и никогда обратно в Span<T> или T[].
Типичные ошибки
- ✗Считать, что span ходит туда и обратно — нет ни
ReadOnlySpan<T>вSpan<T>, ниSpan<T>вT[] - ✗Думать, что это неявный оператор уровня библиотеки, а не языковая конверсия
- ✗Упускать, что выигрыш — получатели extension-методов и вывод типов в обобщениях
Уточняющие вопросы
- →Почему превращение конверсии в языковую меняет вывод типов в обобщениях?
- →Как получить записываемый
Span<T>изReadOnlySpan<T>, если он действительно нужен?
SeniorТеорияРедкоЧто числовой абстракции generic math нужно от библиотеки и что — от языка?
Что числовой абстракции generic math нужно от библиотеки и что — от языка?
Две половины: интерфейсы System.Numerics из .NET 7 — прежде всего INumber<T> — абстрагируют операторы, а static abstract-члены интерфейса из C# 11 позволяют их потребовать. Вместе один обобщённый метод с ограничением INumber<T> суммирует любой числовой T.
Типичные ошибки
- ✗Смешивать половины —
INumber<T>это .NET 7, аstatic abstract-члены это C# 11 - ✗Думать, что диспетчеризация операторов идёт в рантайме, а не разрешается на аргумент типа
- ✗Считать, что нужно особое ограничение на оператор, а не ограничение интерфейсом
Уточняющие вопросы
- →Почему
static abstract-член работает лишь через параметр типа, а не через ссылку на интерфейс? - →Как JIT удерживает обобщённый метод generic math быстрым, когда
T— значимый тип?
SeniorТеорияРедкоПочему source-генератор, работающий при компиляции, обыгрывает рефлексию для JSON при ahead-of-time (AOT) компиляции?
Почему source-генератор, работающий при компиляции, обыгрывает рефлексию для JSON при ahead-of-time (AOT) компиляции?
Source-генератор работает внутри компилятора и выдаёт обычный C#: код сериализатора — настоящий анализируемый исходник, метаданные при старте не строятся. Рефлексия узнаёт типы в рантайме, поэтому триммер не видит, что используется, а AOT нечего компилировать.
Типичные ошибки
- ✗Думать, что source-генератор работает при старте или после сборки, а не внутри компилятора
- ✗Считать, что рефлексия переживает тримминг и AOT без аннотаций и удержания типов
- ✗Полагать, что выигрыш только в скорости, упуская анализируемость, которой требует AOT
Уточняющие вопросы
- →Что
JsonSerializerContextменяет в том, как сериализатор разрешает тип? - →Какие применения рефлексии source-генератор заменить не может?