Элвис, Converting null..., .NET C#
@iksergeyruВот вы создали первое приложение на языке C# и написали нечто похожее на:
string name = Console.ReadLine();
Программа запускается, результат есть, но вот незадача: компилятор выдаёт предупреждение:
warning CS8600: Converting null literal or possible null value to non-nullable type
Что за предупреждение попробуем разобраться в этой заметке.
Детство
Разбираться будем на сложном, для начинающих, примере. Не факт, что получится понять и разобраться с первого раза — вернитесь позже. Не факт, что понимание придёт с первого раза — просмотрите ещё раз. И уж точно не факт, что получится разобраться не написав ни одной строчки кода...
Приступим.
Давайте рассмотрим такой код:
class Number
{
public string Description { get; set; }
public int Value { get; set; }
}
class House
{
public string Description { get; set; }
public Number Number { get; set; }
}
class Street
{
public string Description { get; set; }
public House House { get; set; }
}
class City
{
public string Description { get; set; }
public Street Street { get; set; }
}
class Address
{
public string Description { get; set; }
public City City { get; set; }
}
Замечание: не зацикливайтесь на непонятных словах. Смотрите на картину в целом.
В данном коде описывается модель хранения адреса.
Хранить адрес как строку не подходит, потому что непонятно как в дальнейшем разбирать её на части, а именно: почему именно это будет название города, улицы, или области.
Пусть есть строка «Смоленская», что это означает: улица Смоленская или Смоленская область? Сложно ответить не имея контекста, но если у нас модель, описанная выше, мы точно сможем это определить если
Street street = new() { Description = "Смоленская" };
*Значит улица
Замечание: я выбрал пример с описанием одной улицы, опустив детали всех остальных значений. Принцип будет аналогичным, увеличится количество строк кода (желающие могут изучить исходники)
Итак, мы разобрались с тем, почему для хранения данных лучше использовать сложные модели. Теперь предположим, что нам потребуется получать адреса от какого-то сервиса.
Проблема в этом случае следующая: от сервиса данные могут приходить порционно и какая-то порция может вообще "потеряться" (пользователь не указал, потерялось соединение с сервисом и т д)
Смоделируем работу такого сервиса, т е есть некоторый GetAddress() выдающий адреса с "перебоями".
Замечание: для простоты пояснения, адрес выдаётся всегда один. При желании можно использовать любой источник данных, из которой извлекаются значения.
Полный адрес: г. Смоленск ул. Пржевальского Дом 4
Код:
for (int i = 0; i < 10; i++)
{
Console.WriteLine(GetAddress());
}
Выдаёт
1. 2. 3. г. Смоленск ул. Пржевальского Дом 4 4. г. Смоленск ул. Пржевальского 5. г. Смоленск ул. Пржевальского 6. г. Смоленск ул. Пржевальского Дом 4 7. г. Смоленск 8. г. Смоленск ул. Пржевальского Дом 4 9. г. Смоленск 10. г. Смоленск ул. Пржевальского Дом 4
В первых двух случаях адреса нет вообще, в 7 и 9 неизвестно ничего кроме города, а в 4 и 5 потерялись данные о доме.
Теперь давайте подумаем, как извлекать номер дома? Как описать это кодом?
Примерно так:
var n = GetAddress() .City .Street .House .Number .Value;
Есть очень большая проблема, а именно: мы знаем, что часть адреса может "потеряться", а значит обращение к следующей части адреса без проверки её существования приведёт к ошибке
NullReferenceException: Object reference not set to an instance of an object.
Замечание: в моём случае из 100 запросов, получалось ≈60 ошибок. Такой сервис не имеет права на жизнь, но как академический пример очень показателен.
Для того чтобы правильно обрабатывать результат этого сервиса, нужно написать проверку на получение каждого звена, т е
Address address = GetAddress();
string failInfo = String.Empty;
int value = 0;
if (address != null)
{
var city = address.City;
if (city != null)
{
var street = address.City.Street;
if (street != null)
{
var house = address.City.Street.House;
if (house != null)
{
var number = address.City.Street.House.Number;
if (number != null)
{
value = address.City.Street.House.Number.Value;
}
else
{
failInfo = "No Number";
}
}
else
{
failInfo = "No House";
}
}
else
{
failInfo = "No Street";
}
}
else
{
failInfo = "No City";
}
}
else
{
failInfo = "No Address";
}
if (String.IsNullOrEmpty(failInfo))
{
Console.WriteLine($"Номер дома: {value}");
}
else
{
Console.WriteLine($" failInfo: {failInfo}");
}
Согласитесь, код адовый.
Такой код — следствие того, что номер дома у нас число int, а в числе можно хранить только значение int и ничего другого.
Возможно, нам не нужно хранить информацию о тех данных, которые не пришли, в этом случае код можно переписать так:
Address address = AddressProvider.Instance.GetAddress();
bool failResponse = false;
int value = 0;
if (address != null)
{
var city = address.City;
if (city != null)
{
var street = address.City.Street;
if (street != null)
{
var house = address.City.Street.House;
if (house != null)
{
var number = address.City.Street.House.Number;
if (number != null)
{
value = address.City.Street.House.Number.Value;
}
else
{
failResponse = true;
}
}
else
{
failResponse = true;
}
}
else
{
failResponse = true;
}
}
else
{
failResponse = true;
}
}
else
{
failResponse = true;
}
if (!failResponse)
{
Console.WriteLine($"Номер дома: {value}");
}
else
{
Console.WriteLine($"Какие-то данные отсутствуют");
}
Технически, все ветки else, для нас не имеют никакого значения. И можно в номер дома положить -100, тогда если это значение останется — это будет означать, что при получении адреса произошли какие-то ошибки.
Каскад, if-ов от этого меньше не станет.
Отрочество
Давайте подумаем, что если в value помимо числа, мы сможем хранить что-то отличное от числа? Было бы здорово! Так и подумали разработчики C#, добавили в C# новую фичу и назвали её Nullable-типами.
Что это такое?
Раньше в int можно было хранить только числа, то теперь у нас появляется надстройка в виде
Nullable<Int32>
который, с учётом особенностей платформы, в C# (In32 ≡ int) превращается в
Nullable<int>
который допускает хранение значения null.
Любовь разработчиков к сокращениям привела к тому, что для Nullable<int> добавили более короткую запись — int?.
Замечание: обращаю внимание: тип "int?" — это "синоним" типа "Nullable<int>", тип "int?" не является "синонимом" типа "int".
Теперь у нас есть возможность использовать такую запись
int? data = 1; int? data = null;
и это не вызовет ошибок в отличие от
int value = null
который мгновенно приводит к ошибке компиляции
error CS0037: Cannot convert null to 'int' because it is a non-nullable value type
Как следствие, у нас появляется возможность проверки data на соответствие null т е
if(data == null) { ... }
Для работы с Nullable-типами хорошо подходит "элвис-оператор" — "?."

Смотрите во что теперь превращается каскад if-ов из кода выше:
Address address = GetAddress();
int? n = address?.City?.Street?.House?.Number?.Value;
Console.WriteLine($"value: {n}");
Главное — вспомнить, ради чего вы это читаете, помните? Напоминаю:
предупреждение warning CS8600: Converting null literal or possible null value to non-nullable type при написании string name = Console.ReadLine();
теперь мы подходим к ответу на вопрос, который сформулировали в начале.
Юность
Получив значение n
int? n = address?.City?.Street?.House?.Number?.Value;
оно может быть либо числом, либо null
Т е мы не можем прописать, что-то вроде
int val = n;
потому что int? и int — это разные типы данных и между ними нет неявного приведения.
Мы должны явно извлекать значение при помощи
int val = n.Value;
Написав такую конструкцию мы можем получить ошибку извлечения данных т к n допускает хранение null:
InvalidOperationException: Nullable object must have a value.
Поэтому нужно делать проверку
if (n != null)
{
int val = n.Value;
}
или более синтаксически грамотную конструкцию:
if (n.HasValue)
{
int val = n.Value;
}
Если же вы уверены, что n не может быть null, тогда можно прописать принудительное "извлечение данных" используя конструкцию
int val = n!.Value;
Замечание: в контексте нашего сервиса получения адресов, такое использование недопустимо. Есть несколько этапов на которых можно получить null и попытка извлечь данные из null-значения закончится вылетом всего приложения.
А вот когда мы пишем
string name = Console.ReadLine()!;
зная, что Console.ReadLine() будет использовать для ручного ввода из терминала, принудительное извлечение указать можно, потому что "руками" мы не сможем ввести этот самый "null".
Замечание: если будет использоваться потоковый ввод, например чтение данных из параметров запуска или потоковое чтение из файла — проверки делать обязательно.
Аналогично происходит работа с double?, string? или bool?
Подведите итоги самостоятельно в комментариях к этому посту.
Исходники на GitHub
Благодарю за внимание
ps ещё, есть операторы ?? и ??=, но об этом в другой раз.