Записи и сопоставление с образцом
Записи и value-равенство, позиционные записи, формы сопоставления с образцом, switch-выражения, init-only и required, первичные конструкторы.
12 вопросов
JuniorТеорияОчень частоЧто делает выражение with для record, и насколько глубока копия?
Что делает выражение with для record, и насколько глубока копия?
Выражение with создаёт новый экземпляр — поверхностную копию исходного с заменёнными членами; сам исходный объект не меняется. Компилятор вызывает синтезированный конструктор копирования, поэтому объекты по ссылкам разделяются, а не клонируются.
Типичные ошибки
- ✗Думать, что
withменяет оригинал, а не возвращает новый экземпляр - ✗Ждать глубокой копии — объекты, на которые ссылается record, разделяются с копией
- ✗Полагать, что неупомянутые члены сбрасываются в значения по умолчанию, а не переносятся
Уточняющие вопросы
- →Почему выражение
withможет присвоитьinit-свойства, которые иначе заморожены? - →Можно ли повлиять на копирование в
with, написав конструктор копирования самому?
MiddleТеорияОчень частоЧем record отличается от class, когда вы сравниваете два экземпляра?
Чем record отличается от class, когда вы сравниваете два экземпляра?
record — по-прежнему ссылочный тип, но компилятор синтезирует Equals, GetHashCode и ==, сравнивающие его тип и каждое поле, поэтому два record с равным содержимым равны. Обычный class сохраняет равенство по ссылке: два разных экземпляра никогда не равны.
Типичные ошибки
- ✗Думать, что record — значимый тип; record class — ссылочный тип
- ✗Ждать, что равенство record обходит граф объектов; член
List<T>всё равно сравнится по ссылке - ✗Полагать, что
==у record по-прежнему сравнивает ссылки
Уточняющие вопросы
- →Почему синтезированное равенство record сравнивает ещё и тип, а не только поля?
- →Что происходит с value-равенством record, если один из его членов — массив?
JuniorТеорияЧастоЧто проверяет type-паттерн is, и как он обходится с null?
Что проверяет type-паттерн is, и как он обходится с null?
Type-паттерн is проверяет тип значения в рантайме и при совпадении присваивает его новой типизированной переменной — if (o is string s), заменяя пару «проверка и приведение». null не совпадает с type-паттерном, поэтому совпадение гарантирует, что значение не null.
Типичные ошибки
- ✗Думать, что
nullсовпадает с type-паттерном — он не совпадает никогда - ✗Считать, что
isпроверяет статический тип, а не тип во время выполнения - ✗Забывать, что объявленная переменная точно присвоена лишь там, где паттерн совпал
Уточняющие вопросы
- →Чем область видимости переменной в
o is string sотличается от отрицанияis not? - →Как property-паттерн надстраивается над type-паттерном, чтобы заглянуть внутрь значения?
MiddleТеорияЧастоЧто даёт позиционный record такого, чего нет у record с обычными свойствами?
Что даёт позиционный record такого, чего нет у record с обычными свойствами?
Позиционный record Point(int X, int Y) синтезирует публичные init-свойства, соответствующий конструктор и Deconstruct, поэтому годится для позиционных паттернов. Ручные свойства ничего этого не дают, зато позволяют выбрать доступность или required.
Типичные ошибки
- ✗Думать, что позиционные параметры становятся полями, а не init-свойствами
- ✗Считать, что record с ручными свойствами теряет value-равенство — оно есть у любого record
- ✗Ждать
Deconstructу record, свойства которого написаны вручную
Уточняющие вопросы
- →Как сохранить позиционный синтаксис, но сменить доступность свойства или добавить валидацию?
- →Почему написанное вручную свойство record не работает в позиционном паттерне?
MiddleТеорияЧастоЧем выражение switch отличается от оператора switch?
Чем выражение switch отличается от оператора switch?
Выражение switch даёт значение: каждая ветвь — pattern => выражение, без case, break и проваливания, поэтому его можно присвоить или вернуть. Оператор switch выполняет операторы. Несовпавшее значение бросает SwitchExpressionException.
Типичные ошибки
- ✗Думать, что несовпавшее выражение
switchоткатывается кdefault, а не бросает исключение - ✗Ждать
breakили проваливания — ветви здесь выражения, а не операторы - ✗Считать, что паттерны и охранники
whenработают только в форме оператора
Уточняющие вопросы
- →Что сообщает компилятор, когда выражение
switchне исчерпывающее? - →Когда оператор
switchвсё же остаётся лучшим выбором?
JuniorТеорияИногдаЧем init-сеттер свойства отличается от readonly-поля?
Чем init-сеттер свойства отличается от readonly-поля?
init-аксессор разрешает присваивание только пока объект инициализируется — конструктор, инициализатор объекта или выражение with. readonly-поле присваивается только при объявлении или в конструкторе, поэтому инициализатор объекта его не тронет.
Типичные ошибки
- ✗Думать, что
initзамораживает объект в рантайме, тогда как проверку делает компилятор - ✗Считать, что инициализатор объекта может присвоить
readonly-поле так же, какinit-свойство - ✗Полагать, что
initделает содержимое свойства глубоко неизменяемым, а не только присваивание
Уточняющие вопросы
- →Как выражение
withумудряется присвоитьinit-свойства в создаваемой копии? - →Делает ли
initнеизменяемым свойство, хранящее изменяемыйList<T>?
MiddleТеорияИногдаЧто сопоставляет каждый из паттернов: type, property, relational и list?
Что сопоставляет каждый из паттернов: type, property, relational и list?
Type-паттерн проверяет тип в рантайме; property-паттерн заглядывает в члены ({ Age: > 18 }); позиционный деконструирует значение; relational сравнивает с константой через and/or; list-паттерн сопоставляет форму последовательности ([1, .., 9]).
Типичные ошибки
- ✗Думать, что паттерны сопоставляются рефлексией в рантайме, а не скомпилированными проверками типа и членов
- ✗Не знать, что list-паттерн поддерживает срез
..и вложенные паттерны внутри - ✗Считать, что паттерны нельзя комбинировать через
and,orиnot
Уточняющие вопросы
- →Как позиционный паттерн находит нужный ему метод
Deconstruct? - →Когда компилятор считает набор паттернов исчерпывающим?
MiddleТеорияИногдаЧто на самом деле объявляет primary-конструктор у обычного class, не record?
Что на самом деле объявляет primary-конструктор у обычного class, не record?
У class параметры primary-конструктора — параметры конструктора, захваченные в область видимости всего тела: их видно в инициализаторах полей и в членах. Публичными свойствами они НЕ становятся — это делает только позиционный record.
Типичные ошибки
- ✗Ждать, что параметры primary-конструктора class станут публичными свойствами, как у record
- ✗Думать, что primary-конструкторы доступны только для record
- ✗Полагать, что каждый параметр захватывается в поле, даже если его не использует ни один член
Уточняющие вопросы
- →Когда компилятор действительно захватывает параметр primary-конструктора в поле?
- →Как проверить аргумент primary-конструктора до того, как он будет сохранён?
MiddleТеорияИногдаЧто required у члена требует, и кто именно выполняет эту проверку?
Что required у члена требует, и кто именно выполняет эту проверку?
required заставляет вызывающего присвоить член в инициализаторе объекта, иначе код не соберётся: проверяет компилятор, а не рантайм. Он ортогонален init: такой член может остаться изменяемым. Конструктору, который его задаёт, нужен [SetsRequiredMembers].
Типичные ошибки
- ✗Думать, что
requiredподразумеваетinit—required-член может остаться изменяемым - ✗Ждать исключения в рантайме; на деле это ошибка компиляции на месте вызова
- ✗Забывать, что конструктору, который уже задаёт член, нужен
[SetsRequiredMembers]
Уточняющие вопросы
- →Как ведёт себя
required, когда производный тип добавляет или скрывает члены? - →Почему рефлексия или десериализатор всё же могут оставить
required-член незаданным?
SeniorТеорияИногдаПочему базовый record никогда не равен производному record с теми же значениями полей?
Почему базовый record никогда не равен производному record с теми же значениями полей?
Синтезированное равенство record сравнивает скрытое свойство EqualityContract — тип в рантайме — раньше полей. Производный record его переопределяет, поэтому контракты расходятся и равенство рушится в обе стороны. Сравнение типа сохраняет симметричность ==.
Типичные ошибки
- ✗Думать, что равенство record сравнивает лишь поля и игнорирует тип во время выполнения
- ✗Полагать, что сравнение производного экземпляра через базовый тип может пройти успешно
- ✗Считать, что производные record откатываются к равенству по ссылке
Уточняющие вопросы
- →Как заставить базовый record признать производный экземпляр равным, и что тогда сломается?
- →Как выглядит
EqualityContractу sealed record?
SeniorТеорияРедкоКак required-члены уживаются с конструктором, который их уже задаёт?
Как required-члены уживаются с конструктором, который их уже задаёт?
Конструктор по умолчанию не удовлетворяет required: члены всё равно присваивает вызывающий в инициализаторе. [SetsRequiredMembers] сообщает, что конструктор задаёт их все, и снимает обязанность. Primary-конструктор class её не снимает: его параметры — не свойства.
Типичные ошибки
- ✗Думать, что конструктор, присваивающий
required-член, освобождает вызывающих без[SetsRequiredMembers] - ✗Ждать, что параметры primary-конструктора class удовлетворят
required-члены - ✗Вешать
[SetsRequiredMembers]на конструктор, который на деле задаёт не все из них
Уточняющие вопросы
- →Что ломается, если
[SetsRequiredMembers]повесить на конструктор, пропускающий один член? - →Как конструктор производного типа наследует обязанность по
required-членам от базового?
SeniorТеорияРедкоЧто делает выражение switch исчерпывающим, и как ведут себя null и несовпавшие значения?
Что делает выражение switch исчерпывающим, и как ведут себя null и несовпавшие значения?
Компилятор доказывает полноту по паттернам ветвей; если не может — предупреждает (CS8509), а несовпавшее значение бросает SwitchExpressionException. Type-паттерн не совпадает с null, поэтому возможно-null входу нужна ветвь null или ветвь _.
Типичные ошибки
- ✗Ждать, что неисчерпывающее выражение вернёт
default, а не бросит исключение - ✗Считать
enumисчерпанным, когда у всех объявленных членов есть ветвь — приведение даёт любое значение - ✗Думать, что type-паттерн совпадает с
nullи явная ветвьnullне нужна
Уточняющие вопросы
- →Почему переменная
enumможет держать значение, которого не называет ни один член? - →Как ветвь
_меняет то, что компилятор сможет сообщить вам при появлении нового случая?