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

