Nullable: значимые типы и nullable reference types
↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая
Зачем это на собесе
NullReferenceException— самая частая ошибка в .NET. Nullable reference types (NRT) переносят её обнаружение на этап компиляции. Спросят разницуint?иstring?и операторы?.,??,!.
Объяснение
Nullable value types — Nullable<T>
Value type не может быть null. int? = Nullable<int> — структура с полями HasValue и Value.
int? age = null;
if (age.HasValue) Console.WriteLine(age.Value);
int years = age ?? 0; // значение по умолчанию
int? next = age + 1; // «поднятые» операторы: null + 1 = null
age.GetValueOrDefault(); // 0В EF Core int? отображается на nullable-колонку, int — на NOT NULL.
Nullable reference types (C# 8+)
Включаются в проекте: <Nullable>enable</Nullable>. После этого:
string— «не может быть null», компилятор предупреждает при присвоении null.string?— «может быть null», компилятор требует проверки перед разыменованием. Это только статический анализ: в рантаймеstringиstring?— один тип, проверок нет.
public class Patient
{
public required string LastName { get; init; } // обязателен при создании
public string? MiddleName { get; init; } // может отсутствовать
}
string GetDisplayName(Patient p)
{
// p.MiddleName.Length; // warning CS8602: возможное разыменование null
return p.MiddleName is null ? p.LastName : $"{p.LastName} {p.MiddleName}";
}Операторы
var city = patient?.Address?.City; // null-conditional: null, если по пути null
var len = patient?.Name?.Length ?? 0; // null-coalescing
options.Timeout ??= TimeSpan.FromSeconds(30); // присвоить, если null
handler?.Invoke(this, args); // вызвать делегат, если есть
string name = maybeName!; // null-forgiving: «я уверена, что не null» — отключает предупреждениеАтрибуты для анализатора
bool TryGetPatient(Guid id, [NotNullWhen(true)] out Patient? patient) { ... }
if (TryGetPatient(id, out var p)) Console.WriteLine(p.LastName); // здесь p не null, без предупреждения
[return: NotNullIfNotNull(nameof(input))] string? Normalize(string? input) => input?.Trim();
static void ThrowIfNull([NotNull] object? value) { ... }
ArgumentNullException.ThrowIfNull(dto); // guard-метод
ArgumentException.ThrowIfNullOrWhiteSpace(code);Паттерны проверки
if (patient is null) return NotFound(); // предпочтительно: не вызывает перегруженный ==
if (patient is not null) { ... }
if (patient is { Address.City: var city }) Console.WriteLine(city); // property patternНюансы и подводные камни
!(null-forgiving) — «заглушить» компилятор. Частое!= проблема в дизайне. Допустимо в тестах и в местах, где инициализацию гарантирует фреймворк (например,DbSet<T>в DbContext:= null!).- Десериализация JSON может положить null в не-nullable свойство: NRT этого не видит. Валидируйте DTO (
required, FluentValidation). - EF Core учитывает NRT: не-nullable
string→ колонкаNOT NULL. Включение NRT в старом проекте может сгенерировать миграцию, меняющую nullability колонок. - Для generic-кода:
T?для неограниченного T означает «default(T)», а не Nullable. defaultдля struct — «нулевые» поля, а не null. Проверкаif (dateTime == null)дляDateTimeвсегда false.
Тестирование
[Fact]
public void Display_name_without_middle_name()
{
var p = new Patient { LastName = "Иванова", MiddleName = null };
GetDisplayName(p).Should().Be("Иванова");
}Вопросы с ответами
Чем int? отличается от string?
int? — это реальный тип Nullable<int>, структура с HasValue и Value. string? — аннотация для статического анализа при включённых NRT: в рантайме это тот же string.
Что дают nullable reference types?
Компилятор отслеживает, где может быть null, и предупреждает о возможных NullReferenceException до запуска. Контракты «может быть null» становятся явными в сигнатурах.
Что делает оператор ! после выражения?
Null-forgiving operator: говорит компилятору считать значение не-null и убирает предупреждение. На рантайм не влияет.
Почему is null лучше, чем == null?
Оператор == может быть перегружен в типе, а is null всегда проверяет именно ссылку. К тому же это единый стиль с pattern matching.
Гарантирует ли не-nullable свойство, что там не будет null после десериализации?
Нет. NRT — только анализ компилятора. JSON-сериализатор может записать null. Нужны required-свойства, настройки сериализатора и валидация.
Связанные темы
- Предыдущая: Строки: иммутабельность, StringBuilder, интерполяция, сравнение · Следующая: Методы и параметры: ref, out, in, params, именованные аргументы
- Records и required: Records, init, with, required, primary constructors
- Валидация DTO: Валидация: DataAnnotations и FluentValidation
- Моделирование в EF Core: Моделирование: конвенции, Fluent API, связи, owned types, NamingConventions
- Optional chaining в JS: ES6+ возможности