Содержание
Ссылочные типы могут иметь значение null
, если ссылка не ссылается ни на какой объект. В противоположность им значимые типы не могут иметь значение null
.
1 2 | string s = null; // Допустимо, строка - ссылочный тип int i = null; // Ошибка при компиляции - int не может иметь значение null |
Чтобы сделать возможным для значимых типов принимать значение null
, нужно использовать специальную конструкцию — тип, допускающий значение null (nullable type). Чтобы превратить значимый тип в тип, допускающий значение null, нужно при объявлении переменной непосредственно после указания ее (значимого) типа поставить знак ?
:
1 2 | int? i = null; // тип, допускающий значение null - Nullable Type Console.WriteLine (i == null); // True |
Структура Nullable<T>
T?
преобразуется в System.Nullable<T>
. Nullable<T>
— легкая неизменяемая структура, имеющая только два поля: Value
и HasValue
.
1 2 3 4 5 6 7 | public struct Nullable<T> where T : struct { public T Value {get;} public bool HasValue {get;} public T GetValueOrDefault(); public T GetValueOrDefault (T defaultValue); } |
Таким образом код:
1 2 | int? i = null; Console.WriteLine (i == null); // True |
преобразуется в:
1 2 | Nullable<int> i = new Nullable<int>(); Console.WriteLine (! i.HasValue); // True |
Попытка получить значение поля Value
если поле HasValue
возвращает false
приведет к выбрасыванию исключения InvalidOperationException
. Метод GetValueOrDefault()
вернет значение Value
если HasValue
возвращает true
, в противном случае он вернет new T()
или другое, кастомное, значение по умолчанию. Значение по умолчанию для T?
— null
.
Преобразование типов, допускающих значение null
Преобразование из T
в T?
выполняется автоматически, а преобразование из T?
в T
требует явного приведения:
1 2 | int? x = 5; // автоматически int y = (int)x; // явное приведение |
Явное приведение объекта, допускающего значение null, эквивалентно вызову его свойства Value
. При этом, если HasValue
возвращает false
, будет выброшено исключение InvalidOperationException
.
Приведение к объектному типу и восстановление значения типов, допускающих значение null (Boxing/Unboxing Nullable Values)
Когда T?
приводится к объектному типу, объект в хипе будет содержать значение типа T
, а не T?
. Такого рода оптимизация возможна благодаря тому, что приведенное к объектному типу значение уже является ссылочным типом и может принимать значение null
.
Для типов, допускающих значение null, восстановление значения из объектного образа (unboxing) можно сделать с помощью оператора as
. При этом, если операция не возможна, будет возвращено значение null
.
1 2 3 | object o = "string"; int? x = o as int?; Console.WriteLine (x.HasValue); // False |
Поднятие операторов (Operator Lifting)
Для структуры Nullable<T>
не определены такие операторы как <
, >
и даже ==
. Несмотря на это выражения с этими операторами будут вполне корректно скомпилированы и выполнены:
1 2 3 | int? x = 5; int? y = 10; bool b = x < y; // true |
Это возможно благодаря тому, что компилятор заимствует или поднимает (lift) операторы у соответствующих значимых типов. Семантически пример выше преобразуется в следующий код:
1 2 3 | bool b = (x.HasValue && y.HasValue) ? (x.Value < y.Value) : false; |
Т.е. если x
и y
имеют значения, они сравниваются как int
, если нет — выражение вернет false
.
Поднятие операторов означает, то можно использовать операторы типа T
для типа T?
. Можно переопределить операторы для типа T?
, чтобы реализовать специальную логику для операций со значением null
, однако в большинстве случаев лучше будет использовать стандартную логику компилятора. Поведение компилятора, если один или оба операнда будут иметь значение null
, зависит от типа оператора.
Операторы равенства (==, !=)
Поднятые операторы равенства со значениями null
ведут себя также как операторы ссылочных типов:
- два значения
null
равны - если только один операнд имеет значение
null
— операнды не равны - если оба операнда имеют не нулевые значения — сравниваются их значения (свойства
Value
)
1 2 | Console.WriteLine ( null == null); // True Console.WriteLine ((bool?)null == (bool?)null); // True |
Операторы отношения (<, <=, >=, >)
Операторы отношения исходят из принципа бессмысленности сравнивать значения null
: если хотя бы один операнд (или оба) имеет значение null
, выражение вернет false
:
1 2 3 4 5 | bool b = x < y; // Преобразуется в: bool b = (x == null || y == null) ? false : (x.Value < y.Value); // если x = 5, а y = null, то b = false |
Все остальные операторы (+, –, *, /, %, &, |, ^, <<, >>, +, ++, —, !, ~)
Все остальные операторы вернут null
, если хотя бы один операнд имеет значение null
:
1 2 3 4 5 | int? c = x + y; // Преобразуется в: int? c = (x == null || y == null) ? null : (int?) (x.Value + y.Value); // если x = 5, а y = null, то c = null |
Сочетание операторов
Благодаря возможности автоматического приведения T
к T?
можно сочетать в одном выражении обычные значимые типы и типы, допускающие значение null
:
1 2 3 | int? a = null; int b = 2; int? c = a + b; // c = null - эквивалентно выражению a + (int?)b |
Тип bool? с операторами & и |
Операторы &
и |
при взаимодействии с операндами типа bool?
расценивают null
как неизвестное значение. В связи с этим выражение null | true
вернет true
т.к.:
- если неизвестное значение
false
, результат будетtrue
- если неизвестное значение
true
, результат будетtrue
По тем же причинам выражение null & false
вернет false
.
1 2 3 4 5 6 7 | bool? n = null, f = false, t = true; Console.WriteLine (n | n); // null Console.WriteLine (n | f); // null Console.WriteLine (n | t); // True Console.WriteLine (n & n); // null Console.WriteLine (n & f); // False Console.WriteLine (n & t); // null |
Оператор объединения со значением NULL (??)
Оператор ?? называется оператором объединения со значением null. Он может использоваться как с типами, допускающими значение null, так и с ссылочными типами. Оператор вернет свой левый операнд если он не равен null, в противном случае он вернет правый операнд.
1 2 3 4 | int? x = null; int y = x ?? 5; // y = 5 int? a = null, b = 1, c = 2; Console.Write (a ?? b ?? c); // 1 (первое не нулевое значение) |