Равенство, null и dynamic
Эта тема собирает места, где C# требует точной модели типа, а не интуиции. Равенство раздваивается на сравнение по ссылке и по значению, и от категории типа зависит, какое из них получите по умолчанию. null для значимого типа выражается через Nullable<T> — обёртку-struct, а не через нулевую ссылку. Неизменяемость означает, что «правка» строки на деле создаёт новый объект. А var, object и dynamic различаются не написанием, а моментом, когда разрешается тип и проверяются члены.
Общий стержень всех слоёв — различие значимых и ссылочных типов и разница между тем, что знает компилятор (статический тип), и тем, что есть у объекта в рантайме (фактический тип). Кандидат, который держит это различие в голове, отвечает на каверзные вопросы про == / Equals, int? и var / dynamic уверенно, а не угадывает. Полная карта — в слоях ниже.
Карта темы
- Равенство по значению — значимые типы сравниваются по значению; контракт
Equals/GetHashCodeи records. - Равенство по ссылке —
==противEquals,ReferenceEquals, почемуstringсравнивает по значению. - Nullable-типы —
Nullable<T>/int?какstruct,HasValue/Value, операторы??,?.. - Неизменяемость — состояние не меняется после создания;
string,StringBuilder,readonly, records. - dynamic и object — статический базовый тип против связывания членов в рантайме.
- Вывод типов (var) —
varвыводит статический тип при компиляции и не являетсяdynamic.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Ждать, что == для двух разных экземпляров класса с равными полями вернёт true | По умолчанию классы сравниваются по ссылке — вернётся false, пока ==/Equals не переопределены |
Переопределить Equals, забыв GetHashCode | Объект «теряется» в Dictionary/HashSet — нарушен контракт «равные объекты дают равный хеш» |
Думать, что == виртуален и диспетчеризуется, как Equals | == — статический оператор, разрешается по типу ссылки при компиляции, а не по фактическому типу |
Читать .Value у пустого int? | Бросает InvalidOperationException; сначала проверяют HasValue либо берут ??/GetValueOrDefault() |
Считать int? ссылочным типом | Nullable<T> — это struct; null здесь означает HasValue == false, а не нулевую ссылку |
Думать, что Replace/ToUpper меняют исходную строку | string неизменяем — каждый метод выделяет новый экземпляр, исходный не тронут |
Путать var с dynamic | var — статический тип, выведенный при компиляции; проверки на месте, в отличие от dynamic |
Значение для собеседований
Эти вопросы любят задавать «на понимание», потому что интуитивный ответ обычно неверен. Кандидат, который различает статический и фактический тип и помнит, что значимые и ссылочные типы ведут себя по-разному, отвечает точно, а не наугад.
Что обычно проверяют:
- Когда
==даёт ссылочное, а когда — значное равенство, и почемуstring— особый случай. - Зачем при переопределении
Equalsобязательно переопределятьGetHashCode. - Что
int?— этоstructNullable<T>, и как безопасно прочитать его значение. - Что значит неизменяемость
stringи когда братьStringBuilder. - Чем
var(статический вывод при компиляции) отличается отdynamic(связывание в рантайме).
Типичный неверный ответ: «var — это динамическая типизация, тип определяется в рантайме». Это открывает разговор о том, что var — чисто компиляторный вывод статического типа, эквивалентный явному имени типа, а рантайм-связывание даёт совсем другое ключевое слово — dynamic.