Что такое nullable reference types и как они отличаются от Nullable<T>?
Nullable<T> (int?) — рантаймовая структура для value types, добавляет реальное поле HasValue. Nullable reference types (string?) — только статический compile-time анализ в C# 8+, в IL разницы с string нет.
Nullable<T> — для значимых типов
Nullable<T> (сокращение T?) — структура, обёртывающая значимый тип (int, bool, DateTime) и добавляющая возможность хранить null. Это полностью рантаймовый механизм: в IL генерируется дополнительное поле HasValue.
int? age = null; // Nullable<int>
bool hasValue = age.HasValue; // false
int safe = age ?? 0; // 0
Nullable reference types (NRT) — для ссылочных типов
Введены в C# 8 / .NET 6. Ссылочные типы всегда могли быть null в рантайме, но компилятор никак не предупреждал. NRT добавляет статический анализ: аннотация string? говорит компилятору «эта переменная может быть null», а string — «гарантированно не null». В рантайме разницы нет — это чисто compile-time информация.
#nullable enable
string nonNull = "hello"; // предупреждение при присвоении null
string? maybeNull = null; // ок
Console.WriteLine(maybeNull.Length); // CS8602: Dereference of possibly null
Console.WriteLine(maybeNull?.Length ?? 0); // безопасно
Включение NRT в проекте
В .csproj добавляется тег <Nullable>enable</Nullable>. Можно также использовать директивы на уровне файла.
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
Ключевые различия
- Nullable<T> — рантайм, структура, только для value types.
typeof(int?)отличается отtypeof(int). - NRT — только статический анализ, нет рантайм-представления.
typeof(string?)==typeof(string). int?нельзя разыменовать без проверки — компилятор и рантайм бросят ошибку.string?— компилятор предупреждает, рантайм — нет (пока вы не вызовете метод на null).
Null-forgiving operator !
Подавляет предупреждение компилятора о возможном null. Используется, когда вы знаете, что значение не null, но анализ не может это доказать (например, после внешней инициализации).
string? input = GetInput();
string trimmed = input!.Trim(); // явное утверждение «не null»
NRT и EF Core
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty; // required, not null
public string? Bio { get; set; } // nullable column
}
Подводные камни
- Включение NRT в существующем проекте генерирует сотни предупреждений — лучше мигрировать файл за файлом с помощью директив
#nullable enable/disable. - Использование
!оператора без реальной гарантии не null приводит кNullReferenceExceptionв рантайме — анализатор молчит, а ошибка всё равно возникает. - Рефлексия и сериализаторы (JSON, XML) не видят NRT-аннотаций:
typeof(string?)иtypeof(string)неотличимы — нужны отдельные атрибуты или соглашения. - Сторонние библиотеки без NRT-аннотаций возвращают ссылочные типы без пометки — компилятор трактует их как «nullable oblivious» и не предупреждает.
- Десериализация JSON может создать объект с
nullвstring-поле (без?), нарушая контракт NRT без compile-time ошибки. - Generic-типы требуют ограничений:
T?означает разное вwhere T : classиwhere T : structконтекстах. - Nullable-аннотации не защищают от null при работе с нативными interop-вызовами и небезопасным кодом.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.