Nullable: значимые типы и nullable reference types

↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая

Обязательно~6 мин чтенияУровень: junior

Зачем это на собесе

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("Иванова");
}

Вопросы с ответами

Связанные темы