Обработка значения null и nullable-типы
Практические правила
- Включайте nullable-анализ в новых проектах (
<Nullable>enable</Nullable>). - На границе системы валидируйте входные DTO.
- В доменной логике старайтесь работать с не-null значениями.
- Для коллекций предпочитайте пустую коллекцию вместо
null.
Мини-шаблон безопасной обработки
public string GetDisplayName(User? user)
{
if (user is null)
{
return "Аноним";
}
return string.IsNullOrWhiteSpace(user.Name) ? "Без имени" : user.Name;
}
Смежные статьи
- Преобразование типов и система типизации
- Работа с типами
- Объектно-ориентированное программирование в C#
Обработка значения null и nullable-типы
Разработчику АрхитекторуОбработка значения null и nullable-типы
Что такое null?
null — это специальное значение, которое означает отсутствие ссылки на объект. Это не число, не пустая строка, не логическое значение, а именно отсутствие значения.
string name = null; // Переменная name не указывает ни на какой объект в памяти
Разбор:
string- ссылочный тип, поэтому переменная хранит ссылку на объект, а не символы "внутри себя".name = nullозначает "ссылка отсутствует", а не "пустая строка".- Комментарий подчёркивает, что объект в памяти не создан и переменная никуда не указывает.
- При попытке вызвать метод у
name(например,name.Length) без проверки возникнетNullReferenceException.
Важно: null может существовать только для ссылочных типов (reference types), потому что они хранят указатель на объект в куче (heap). Значимые типы (value types), как правило, не могут быть null, так как всегда содержат какое-то значение.
Ссылочные типы и null
Ссылочные типы — это типы, экземпляры которых хранятся в куче (heap), а переменная содержит ссылку (указатель) на этот объект. Именно они могут быть null - string, object, классы, интерфейсы, делегаты, массивы.
Person person = new Person(); // ссылка на объект
Person another = null; // ссылка ни на что не указывает
Разбор:
new Person()создаёт экземпляр класса в куче и возвращает ссылку на него.- Переменная
personсодержит валидную ссылку, поэтому через неё можно безопасно обращаться к членам объекта. another = nullпоказывает противоположный сценарий: переменная есть, но объекта за ней нет.- Такой контраст полезен, чтобы понять разницу между "объект создан" и "переменная объявлена, но пустая".
Значимые типы и null
Значимые типы (value types) хранят сами данные, а не ссылку. Они размещаются на стеке (или в структуре). По умолчанию значимые типы не могут быть null, и это соответственно касается int, double, bool, char, DateTime, struct, enum.
int age = 25; // значение хранится напрямую
int number = null; // ОШИБКА компиляции!
Разбор:
int- значимый тип: значение хранится напрямую, а не через ссылку.age = 25корректно инициализирует переменную конкретным числом.int number = nullне компилируется, потому что обычныйintне допускаетnull.- Чтобы хранить "число или отсутствие числа", нужен
int?(Nullable<int>).
Чтобы сделать значимый тип "nullable" (допускающим значение null), C# предоставляет:
Nullable<T> — обобщённый шаблон:
Nullable<int> age = null;
Разбор:
Nullable<int>оборачивает типintи добавляет возможность хранитьnull.- Здесь переменная
ageявно инициализируется как "значение отсутствует". - Это базовая форма, эквивалентная сокращённой записи
int? age = null;. - Такой подход применяют для необязательных полей — возраст, дата завершения, рейтинг и т.д.
Nullable<T>— это структура, которая может хранить либо значение типа T, либо null. Имеет два свойства:- HasValue — true, если значение задано.
- Value — само значение (вызовет исключение, если HasValue == false).
Nullable<int> age = 30;
if (age.HasValue)
{
Console.WriteLine($"Возраст: {age.Value}");
}
else
{
Console.WriteLine("Возраст не указан");
}
Разбор:
HasValueбезопасно проверяет, есть ли внутриNullable<T>реальное значение.- В ветке
ifчтениеage.Valueдопустимо, потому что уже подтверждено наличие значения. - Ветка
elseобрабатывает сценарийnull, не допуская падения приложения. - Такой шаблон (
HasValue->Value) полезен при явной развилке бизнес-логики.
Сокращённый синтаксис — T?
Начиная с C# 2, можно использовать сокращение:
int? age = null; // эквивалентно Nullable<int>
bool? isActive = true;
DateTime? birthDate = null;
Разбор:
- Суффикс
?у значимого типа включает nullable-режим (T?->Nullable<T>). int?,bool?,DateTime?позволяют хранить либо значение, либоnull.isActive = trueпоказывает, что nullable-тип не ограничен толькоnull: обычные значения тоже допустимы.- Такой синтаксис компактнее и читается лучше, чем полная запись
Nullable<T>. Это синтаксический сахар, компилятор преобразуетint?→Nullable<int>.
Операции с Nullable-типами
С Nullable<T> можно выполнять соответствующие операции.
Арифметика: если один из операндов null, результат — null.
Сравнения: ==, != работают корректно с null.
int? a = 5;
int? b = null;
int? result = a + b; // result == null
bool isEqual = (a == b); // false
bool isNull = (b == null); // true
Разбор:
- Арифметика с
Nullable<T>"проталкивает"null: если один операндnull, результат тожеnull. - Выражение
a + bне падает, а возвращаетnull, что делает вычисления предсказуемыми. ==и!=у nullable-типов корректно учитывают отсутствие значения.isNullдемонстрирует самый прямой способ проверки nullable-переменной наnull. Value — опасен, если HasValue == false, выбросит InvalidOperationException.
GetValueOrDefault() — вернёт значение или default(T) (например, 0 для int).
GetValueOrDefault(defaultValue) — вернёт значение или указанное значение по умолчанию.
int? number = null;
int value1 = number.Value; // Исключение!
int value2 = number.GetValueOrDefault(); // 0
int value3 = number.GetValueOrDefault(42); // 42
Разбор:
number.Valueопасен без проверки: приnullвыбрасываетсяInvalidOperationException.GetValueOrDefault()возвращаетdefault(int), то есть0.- Перегрузка
GetValueOrDefault(42)задаёт явное "бизнес-значение по умолчанию". - Пример показывает, как заменить рискованный доступ к
Valueна безопасные альтернативы. Условный оператор доступа: ?. (Null-conditional member access, Null-условный оператор) предоставляет безопасный доступ к свойствам и элементам, если они не null:
string name = person?.Name;
Разбор:
?.(null-conditional) сначала проверяетpersonнаnull, и только затем читаетName.- Если
person == null, выражение сразу возвращаетnull, а не вызывает исключение. - Это компактная замена шаблону
person != null ? person.Name : null. - Оператор особенно полезен в цепочках обращения к вложенным объектам.
Если
person != null, то person.Name возвращается.
Если person == null, выражение возвращает null.
Тип результата: string? (ссылочный тип, допускающий null)
Person person = null;
string name = person?.Name; // null
int? length = person?.Name?.Length; // null (двойная проверка)
// Вызов метода
person?.PrintInfo();
// Вызов делегата
Action action = null;
action?.Invoke(); // безопасный вызов
Разбор:
person?.Name?.Lengthвыполняется "ступенчато": если любой узел цепочкиnull, итог тожеnull.int? lengthвыбран правильно, потому что результат длины может отсутствовать.person?.PrintInfo()не делает вызов, еслиperson == null, тем самым предотвращая падение.action?.Invoke()- стандартный безопасный паттерн вызова делегатов/событий.
Для безопасного доступа к элементам массива, списка, словаря и т.п. используется условный индексный доступ: ?[]
int[] numbers = GetNumbers();
int? first = numbers?[0]; // null, если numbers == null
Dictionary<string, string> dict = null;
string value = dict?["key"]; // null
Разбор:
?[]- null-условный индексатор: работает как безопасная версияarray[index]/dict[key].numbers?[0]вернётnull, если сам массив не создан, вместо исключения.- Для словаря
dict?["key"]даёт тот же принцип безопасного доступа. - Важно понимать: оператор защищает от
dict == null, но не от отсутствующего ключа в уже существующем словаре. Важно —?[]возвращаетT?, еслиT— значимый тип, иначеT(но с возможным null).
Оператор объединения с null — ?? (Null-coalescing operator) возвращает левый операнд, если он не null, иначе — правый.
string message = input ?? "Значение по умолчанию";
Разбор:
??возвращает левый операнд, если он неnull, иначе берёт правый.- Здесь
messageвсегда получает непустую ссылку: либоinput, либо резервный текст. - Оператор полезен для "мягкого" задания дефолта без длинных
if. - Конструкция часто применяется при нормализации входных данных.
Если
input != null→message = input
Если input == null → message = "Значение по умолчанию"
int count = value ?? 0; // если value == null, используем 0
string displayName = user?.Name ?? "Аноним";
DateTime creationDate = record?.Created ?? DateTime.Now;
Разбор:
value ?? 0закрывает сценарий отсутствующего числа и даёт безопасное значение для дальнейшей арифметики.user?.Name ?? "Аноним"комбинирует два оператора: безопасное чтение + fallback.record?.Created ?? DateTime.Nowполезен для дефолтных дат, когда источник может быть пустым.- Такой стиль заметно снижает количество ветвлений
if/elseв коде. Это используется для подстановки значений по умолчанию.
Оператор присваивания объединения
Оператор присваивания объединения с null: ??= (Null-coalescing assignment) появился в C# 8.0. Присваивает правую часть только если левая часть равна null.
List<string> names = null;
names ??= new List<string>(); // инициализируем, если null
Разбор:
??=присваивает значение только когда левая частьnull.- В примере список создаётся лениво: лишний объект не создаётся, если список уже есть.
- Это удобный паттерн инициализации коллекций перед добавлением элементов.
- По смыслу эквивалентно
if (names == null) names = new List<string>();. Работает только с переменными и полями, нельзя использовать с выражениями вроде person?.Name ??= "Unknown".
private string _cache;
public string Data => _cache ??= LoadFromDatabase();
// Ленивая инициализация
public List<int> Items { get; set; }
// где-то в коде:
Items ??= new List<int>();
Разбор:
Data => _cache ??= LoadFromDatabase();реализует ленивую загрузку: чтение из БД произойдёт только один раз.- После первой инициализации
_cacheхранит значение, и повторные вызовы возвращают уже готовые данные. Items ??= new List<int>()делает то же для коллекции: инициализация по требованию.- Такой подход уменьшает накладные расходы и упрощает код инициализации.
Статическая проверка null
До C# 8.0 компилятор не различал, может ли ссылочный тип быть null. Это приводило к частым ошибкам в рантайме. Начиная с C# 8.0, появилась возможность включить статическую проверку null для ссылочных типов.
В .csproj файле можно включить режим Nullable Reference Types, это Null-безопасность на уровне компилятора:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Разбор:
- Секция
<PropertyGroup>задаёт параметры сборки проекта. <Nullable>enable</Nullable>включает nullable-анализ для ссылочных типов на уровне компилятора.- После включения компилятор начинает предупреждать о потенциально опасных местах с
null. - Это профилактика ошибок на ранней стадии, до запуска приложения. или на уровне файла:
#nullable enable
Разбор:
- Директива
#nullable enableвключает nullable-контекст локально, на уровне файла. - Удобно для поэтапной миграции старого проекта: можно включать анализ постепенно.
- В таком режиме
stringтрактуется как non-nullable, аstring?- как nullable. - Компилятор начинает проверять контракты null-безопасности прямо в этом файле.
После этого:
- string name — не должен быть null (non-nullable reference type)
- string? name — может быть null (nullable reference type)
Пример:
#nullable enable
string name = null; // ⚠️ Предупреждение компилятора!
string? optional = null; // OK
void PrintName(string name) // параметр не может быть null
{
Console.WriteLine(name.Length); // компилятор уверен: name != null
}
PrintName(null); // ⚠️ Предупреждение!
Разбор:
string name = nullв nullable-контексте помечается предупреждением: нарушен non-nullable-контракт.string? optional = nullкорректен, потому что тип явно допускает отсутствие значения.- Сигнатура
PrintName(string name)сообщает, что метод ожидает не-null аргумент. - Вызов
PrintName(null)помечается предупреждением на этапе компиляции, что помогает заранее исправить баг. name.Lengthвнутри метода безопасен с точки зрения анализа потока, потому что параметр non-nullable. Компилятор отслеживает инициализацию, проверки на null, присваивания. Поэтому включайте nullable enable в новых проектах, это помогает избежать NullReferenceException ещё на этапе компиляции. И используйте ?., ??, ??= для безопасной работы с null.
Избегайте null там, где можно использовать другие значения - вместо null строки — string.Empty или "", вместо null коллекций — Array.Empty<T>() или new List<T>().