5.05. Обработка null
Обработка null
null — это специальное значение, которое означает отсутствие ссылки на объект. Это не число, не пустая строка, не логическое значение, а именно отсутствие значения.
string name = null; // Переменная name не указывает ни на какой объект в памяти
Важно: null может существовать только для ссылочных типов (reference types), потому что они хранят указатель на объект в куче (heap). Значимые типы (value types), как правило, не могут быть null, так как всегда содержат какое-то значение.
Ссылочные типы — это типы, экземпляры которых хранятся в куче (heap), а переменная содержит ссылку (указатель) на этот объект. Именно они могут быть null - string, object, классы, интерфейсы, делегаты, массивы.
Person person = new Person(); // ссылка на объект
Person another = null; // ссылка ни на что не указывает
Значимые типы (value types) хранят сами данные, а не ссылку. Они размещаются на стеке (или в структуре). По умолчанию значимые типы не могут быть null, и это соответственно касается int, double, bool, char, DateTime, struct, enum.
int age = 25; // значение хранится напрямую
int number = null; // ОШИБКА компиляции!
Чтобы сделать значимый тип «nullable» (допускающим значение null), C# предоставляет:
Nullable<T> — обобщённый шаблон:
Nullable<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("Возраст не указан");
}
Сокращённый синтаксис: T?
Начиная с C# 2, можно использовать сокращение:
int? age = null; // эквивалентно Nullable<int>
bool? isActive = true;
DateTime? birthDate = null;
Это синтаксический сахар, компилятор преобразует int? → Nullable<int>.
С 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
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
Условный оператор доступа: ?. (Null-conditional member access, Null-условный оператор) предоставляет безопасный доступ к свойствам и элементам, если они не null:
string name = person?.Name;
Если 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(); // безопасный вызов
Для безопасного доступа к элементам массива, списка, словаря и т.п. используется условный индексный доступ: ?[]
int[] numbers = GetNumbers();
int? first = numbers?[0]; // null, если numbers == null
Dictionary<string, string> dict = null;
string value = dict?["key"]; // null
Важно: ?[] возвращает T?, если T — значимый тип, иначе T (но с возможным null).
Оператор объединения с null: ?? (Null-coalescing operator) возвращает левый операнд, если он не null, иначе — правый.
string message = input ?? "Значение по умолчанию";
Если input != null → message = input
Если input == null → message = "Значение по умолчанию"
int count = value ?? 0; // если value == null, используем 0
string displayName = user?.Name ?? "Аноним";
DateTime creationDate = record?.Created ?? DateTime.Now;
Это используется для подстановки значений по умолчанию.
Оператор присваивания объединения с null: ??= (Null-coalescing assignment) появился в C# 8.0. Присваивает правую часть только если левая часть равна null.
List<string> names = null;
names ??= new List<string>(); // инициализируем, если null
Работает только с переменными и полями, нельзя использовать с выражениями вроде person?.Name ??= "Unknown".
private string _cache;
public string Data => _cache ??= LoadFromDatabase();
// Ленивая инициализация
public List<int> Items { get; set; }
// где-то в коде:
Items ??= new List<int>();
До C# 8.0 компилятор не различал, может ли ссылочный тип быть null. Это приводило к частым ошибкам в рантайме. Начиная с C# 8.0, появилась возможность включить статическую проверку null для ссылочных типов.
В .csproj файле можно включить режим Nullable Reference Types, это Null-безопасность на уровне компилятора:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
или на уровне файла:
#nullable enable
После этого:
- 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); // ⚠️ Предупреждение!
Компилятор отслеживает инициализацию, проверки на null, присваивания. Поэтому включайте nullable enable в новых проектах, это помогает избежать NullReferenceException ещё на этапе компиляции. И используйте ?., ??, ??= для безопасной работы с null.
Избегайте null там, где можно использовать другие значения - вместо null строки — string.Empty или "", вместо null коллекций — Array.Empty<T>() или new List<T>().