Содержание
Ссылочные типы могут иметь значение 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 (первое не нулевое значение) |

