Записи и сопоставление с образцом
Классы C# создавались для объектов с поведением: у них есть идентичность, и два разных объекта с одинаковым содержимым — по-прежнему два разных объекта. Но огромная часть кода описывает не объекты, а данные: DTO, события, результаты, ключи. Для них важна не идентичность, а содержимое. Ровно это и дают записи: record — по-прежнему ссылочный тип, но компилятор синтезирует ему равенство по значению, копирующий конструктор для with и Deconstruct для позиционной формы.
Вторая половина темы — сопоставление с образцом. Оно выросло из is-проверки в полноценный язык описания формы данных: type-, property-, positional-, relational- и list-паттерны, комбинируемые через and / or / not, и switch-выражение, которое возвращает значение вместо того, чтобы выполнять операторы. Обе половины работают вместе: Deconstruct у записи — это и есть то, за что цепляется позиционный паттерн. Ловушки здесь тонкие и любимые интервьюерами: равенство записей сравнивает тип (скрытый EqualityContract) до полей; with копирует поверхностно; init — контракт компилятора, а не заморозка в рантайме; параметры первичного конструктора у class — не свойства; неисчерпывающее switch-выражение бросает исключение, а не возвращает default. Разбор по слоям — ниже.
Карта темы
- Записи —
recordкак ссылочный тип с синтезированным value-равенством,EqualityContract,withи позиционной формой. - init-only свойства — окно присваивания, ограниченное инициализацией, и чем
initотличается отreadonly-поля. - Обязательные члены —
requiredзаставляет вызывающего заполнить член, и проверяет это компилятор. - Первичные конструкторы — у record параметры становятся свойствами, а у обычного
class— нет. - Сопоставление с образцом — type, property, positional, relational и list-паттерны и как они компилируются.
- switch-выражения —
pattern => value, исчерпывающесть,nullиSwitchExpressionException.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Считать record значимым типом | record (он же record class) — ссылочный тип в куче; значимый — только record struct |
| Ждать от равенства записей обхода графа | Сравнение почленное: поле-List<T> или массив сравнивается по ссылке, а не по содержимому |
Не знать про EqualityContract | Базовая и производная записи с одинаковыми полями никогда не равны — и это не баг, а то, что держит == симметричным |
Ждать от with глубокой копии | Копия поверхностная: объекты, на которые ссылается запись, общие с оригиналом |
Думать, что init замораживает объект в рантайме | Ограничение проверяет компилятор; содержимое List<T> за init-свойством по-прежнему изменяемо |
Считать, что инициализатор объекта может присвоить readonly-поле | Не может: readonly-поле пишется только в объявлении или конструкторе |
Считать, что required подразумевает init | Ортогональные вещи: required-член может остаться полностью изменяемым |
Ждать рантайм-исключения от required | Это ошибка компиляции у вызывающего; конструктору, который сам всё заполняет, нужен [SetsRequiredMembers] |
Ждать, что параметры первичного конструктора class станут публичными свойствами | Не станут — это захваченные в область видимости параметры; свойства делает только позиционный record |
Думать, что null совпадает с type-паттерном | Не совпадает никогда: o is string s на null даёт false, поэтому совпадение гарантирует ненулевое значение |
| Думать, что паттерны работают через рефлексию | Компилятор строит дерево обычных проверок типа и чтений членов — никакой рефлексии в рантайме нет |
Ждать default от неисчерпывающего switch-выражения | Непокрытое значение бросает SwitchExpressionException; компилятор предупреждает об этом (CS8509) |
Считать enum исчерпанным, покрыв все объявленные члены | Приведение (Color)99 даёт значение, которого нет ни в одном члене, — нужна ветка _ |
Значение для собеседований
Записи и паттерны — обязательный блок любого современного C#-интервью, и вопросы здесь почти всегда про механику компилятора: что именно он синтезирует и что происходит с этим в рантайме. Кандидат, который отвечает «record — это класс с автоматически сгенерированными Equals, GetHashCode, копирующим конструктором и EqualityContract», сразу говорит на языке интервьюера.
Что обычно проверяют:
- Чем
recordотличается отclassпри сравнении двух экземпляров и почему. - Зачем в равенстве участвует
EqualityContractи что из этого следует для наследования записей. - Что делает
withи насколько глубока копия. - Что синтезирует позиционная форма
record Point(int X, int Y). - Чем
initотличается отreadonly-поля и кто проверяет ограничение. - Что именно заставляет делать
requiredи почему конструктору нужен[SetsRequiredMembers]. - Что объявляет первичный конструктор у не-record класса.
- Какие формы паттернов есть и как они себя ведут с
null. - Что делает неисчерпывающее
switch-выражение с непокрытым значением.
Типичный неверный ответ: «Первичный конструктор класса создаёт публичные свойства из параметров». Не создаёт. У обычного class это параметры конструктора, захваченные в область видимости всего тела: их видно в инициализаторах полей и в методах, но снаружи obj.Name не появится. Свойства из параметров делает только позиционный record — и именно на этом различии проверяют, понимаете ли вы, что record и class получили одну синтаксическую форму с разной семантикой.