Понятно про null
Что такое null?
Начнем с того что типы в языке С# делятся на ссылочные и типы значений.
Ссылочные типы не хранят в себе само значение, а лишь указывают на него, в отличие от типов значений. Если им не присвоить значение, то они будут указывать на отсутствие значения — null.
Для чего может понадобиться null?
Указание на отсутствие значения может помочь при работе с базами данных, построении клиент серверных-приложений, так как некоторые значения там могут быть необязательны к заполнению. Допустим у нас имеется логин и пароль пользователя, которые обязательны к заполнению, но также есть и электронная почта, которую пользователь может просто не захотеть вводить. В данном случае единственным методов хранить такое значение является указание его отсутствия — использовать NULL.
Почему мы не может использовать null c типом значения?
Как говорилось выше — null указывает на отсутствие значения, но такой подход неприменим для типов значений, т.к. они не указывают на значение, а хранят его, а при отсутствии значения — принимают значение по умолчанию в соответствие со своим типом (например для int — 0, bool — false, char — '\u0000').
Отсутствие значения и ссылочные типы
Существую три основный способа сообщить об отсутствии значения в типах значений(не используя Nullable типы):
- Использование «магических значений».
Данный метод предусматривает поиск уникальных значений, которые были бы альтернативой его отсутствию. Для каждого типа нужно указывать собственное магическое значение — как правило это 0, минимальное или максимальное значение, допустимое типом:
DateTime date = DateTime.MinValue; //Присваиваем переменной минимальное значение для DateTime, которое будет эквивалентно пустому значению
if (date == DateTime.MinValue) //Проверяем переменную, на соответствие нашему "магическому значению"
{
Console.WriteLine("Auch!"); //Оу, похоже у нас пустое значение
}
- Использование отдельного флага типа bool, хранящего в себе информацию пуста ли переменная(false) или имеет значение(true):
DateTime date; //создание самой переменной
bool isValue = false; //создаем флаг - пуста ли переменная data
if (!isValue) //Проверяем, пуста ли переменная
{
Console.WriteLine("Auch!"); //Оу, похоже у нас пустое значение
}
- Создание отдельной ссылочной обертки, допускающей значение null:
class Time //класс, являющийся оберткой для значения
{
public DateTime value { get; set; } //само значение
}
...
Time time = null; //создание обьекта класса Time
if (time == null) //Проверка на null
{
Console.WriteLine("Auch!"); //Оу, похоже у нас пустое значение
}
Все! Мы решили поставленную задачу и полностью преисполнились в своем познании, но если тщательно обдумать все вышеперечисленные способы, то можно понять, что все они навязывают ненужные контракты программистам и требуют дополнительно документирования, а в некоторых случаях и вовсе могут привести к ошибкам.
Далее мы рассмотрим, как решить эту проблему столь же элегантно, как это присуще языку C#.
Некоторые полезные операторы при работе с null
- Оператор условного null
Для начала объявим класс, имеющий лишь одну функцию:
class Hello
{
public string text { get; set; }
public void SayHello()
{
Console.WriteLine("Hello {0}", text);
}
}
Теперь создадим «пустой» экземпляр класса User:
Hello newHello = null;
Попытаемся вызвать функцию через объект, но чтобы избежать ошибки, проверим объект на null:
if (newHello != null)
{
newHello.SayHello();
}
Чтобы избежать постоянных проверок на null мы можем заменить этот код всего одной строкой:
newHello?.SayHello();
Оператор .? сначала проверяет на null, а лишь потом вызывает функцию.
Такой же синтаксис можно использовать для безопасного получения значения полей/свойств у объекта:
Console.WriteLine("text = [{0}]", newHello?.text);Что же мы увидим в консоли? Пустое значение, т.к. значение по факту отсутствует.
- Оператор объединения с null
Бывают ситуации, когда в случае равенства переменной null нужно вывести значение по умолчанию:
Object number = null;
if (number == null)
{
Console.WriteLine(42);
}
else
{
Console.WriteLine(number);
}
Однако данный синтаксис слишком велик, особенное если у нас 10 таких переменных.
Попробуем заменить это нагромождение кода тернарным оператором:
Console.WriteLine(number == null ? 42 : number);
Стало значительно лучше, но с помощью оператора объединения с null можно это сократить код еще больше:
Console.WriteLine(number ?? 42);
Данный оператор имеет следующий синтаксис: переменная ?? значение по умолчанию.
Nullable
Начнем с того, что Nullable является обобщенной структурой, находящейся в пространстве имен System. Он выглядит следующим образом:
public partial struct Nullable<T> where T : struct
Исходя из объявления(where T : struct) можно понять что он принимает универсальные параметры только типа значения.
Создадим переменную Nullable с параметром int:
Nullable<int> number = 42;
Однако существует более лаконичный синтаксис:
int? number = 42;
Попробуем вывести это значение с помощью свойства Value, которое хранит значение объекта:
Console.WriteLine(number.Value); //Получаем 42
Как видим, number ведет себя аналогично типам значений, однако теперь мы можем присвоить ему null:
int? number = null;
if (number == null)
{
Console.WriteLine("Auch!"); //похоже у нас null
}
Такой же результат можно получить с помощью свойства hasValue, которое возвращает false, если переменная не хранит значение и false, если хранит:
if (!number.HasValue)
{
Console.WriteLine("Auch!"); //у нас все еще null
}
Также тип Nallable определяет метод GetValueOrDefault, который аналогичен оператору объединения с null:
Console.WriteLine(number.GetValueOrDefault()); //получим 0, т.к. это значение по умолчанию для типа int
Console.WriteLine(number.GetValueOrDefault(42)); //получим 42, т.к. мы указали его как значение по умолчанию
Console.WriteLine(number ?? 42); //тоже самое, только с оператором "объединения с null”
Преобразования с Nullable
Начнем с явного преобразования из Nullable в реальный тип:
int? a = 1;
int b = (int)a;
Console.WriteLine(b); //1
Все будет работать. Однако попробовав совершить тоже самое действие с null мы получим ошибку:
int? a = null;
int b = (int)a;
Console.WriteLine(b); //Ошибка
Исправить это можно функцией GetValueOrDefault(или оператором объединения с null):
int? a = null;
int b = a.GetValueOrDefault(); //0
int c = a ?? 0; //0
С преобразование из реального типа в null все гораздо проще:
int a = 0;
int? c = a; //0
Выводы
Говоря о преимуществах null, нельзя забывать о том, что он с легкость может привести к ошибкам, связанным с попыткой использования пустого объекта (всеми любимая ошибка NullFeferenceException). Такая ситуация может возникнуть в результате использования вашего кодом другим программистом, особенно если вы не уведомите его о том, что такое знание допустимо в данной ситуации.
