Null-совместимые значимые типы

Значимые типы C#, в отличие от ссылочных, не могут принимать значение null. Однако иногда требуется дополнительное значение, которое указывало бы, что значение переменной значимого типа не определено. Такая ситуация возможна, например, при работе с базами данных, когда числовое поле таблицы может быть пустым ( т.е. помимо диапазона числовых значений содержать NULL ). В этом случае, при отображении данных таблицы на типы C# возникает неопределенность. Какое значение должна принять переменная C#, если из базы данных получен null?

Для разрешения подобных ситуаций в C# были введены null-совместимые значимые типы. Для объявления переменной значимого типа допускающую значение null (неопределенное значение) используется специальный синтаксис – знак вопроса после типа переменной:

 //Ошибка. Тип int не может быть равен null.
int notNullableVariable = null;

// Здесь ошибки нет. int? допускает неопределенное значение - null.
int? nullableVariable = null; 

// Вывод: True
Console.WriteLine( nullableVariable == null );

Структура Nullable

На самом деле выражение int? транслируется в выражение Nullable<int>. Nullable – специальная типизированная структура. На месте int может быть любой другой значимый тип. Ниже представлен код этой структуры, в котором я оставил только то, что нам нужно будет для понимания работы этого типа (полный код структуры Nullable можно посмотреть по ссылке):

public struct Nullable<T> where T : struct {
        private bool hasValue; 
        internal T value;
                
        public bool HasValue {
            get {
                return hasValue;
            }
        } 

        public T Value {
            get {
                if (!hasValue) {               
                     ThrowHelper.ThrowInvalidOperationException(          
                           ExceptionResource.InvalidOperation_NoValue 
                     );
                }
                return value;
            }
        }

        public T GetValueOrDefault() {
            return value;
        }

        public T GetValueOrDefault(T defaultValue) {
            return hasValue ? value : defaultValue;
        }
}

В этом коде определены два поля hasValue (bool) и value (T). Тип T может быть любым типом значения. Поле hasValue определяет присвоено ли значение переменной или нет. Второе поле содержит значение самой переменной типа T. Обратите внимание, что оба поля доступны только для чтения через соответствующие свойства – bool HasValue { get; } и T Value { get; }. Это значит что после создания экземпляра структуры Nullable значения ее полей изменить невозможно. Присваивание нового значения переменной приводит к созданию нового экземпляра Nullable. Следующий код:

int? nullableVariable = null;
nullableVariable = 5;

фактически транслируется в:

Nullable<int> nullableVariable = new Nullable<int>();
nullableVariable = new Nullable<int>( 5 );

Если попытаться извлечь значение Value экземпляра структуры, которому не присвоено значение отличное от null, будет сгенерировано исключение InvalidOperationException.

Метод GetValueOrDefault() возвращает value, если значение полю присвоено не было, будет возвращено значение типа по умолчанию. Метод GetValueOrDefault( T defaultValue ) возвращает значение value, если hasValue == true, в противном случае – значение которое было передано методу при вызове.

Значением по умолчанию для типа T? является null.


Оператор ??

Оператор ?? возвращает левый операнд, если он не равен null, в противном случае возвращает правый операнд. В общем виде синтаксис оператора ?? выглядит следующим образом:

[ переменная ] = [ левый операнд ] ?? [ правый операнд ]

Левый операнд может быть либо ссылочным типом, либо значимым типом допускающим значение null.

Пример использования оператора ??:

bool? b = null;
bool c = b ?? true; // здесь c присваивается true т.к. b == null

Console.WriteLine( c );

// Вывод: true

Преобразование типов допускающих значение null

В исходниках структуры Nullable определены 2 вида преобразований из значимых типов в Nullable<T> ( неявное преобразование ) и обратно ( явное ):

public struct Nullable<T> where T : struct {
    // оператор неявного преобразования
    public static implicit operator Nullable<T>(T value) {
        return new Nullable<T>(value);
    }

    // оператор явного преобразования
    public static explicit operator T(Nullable<T> value) {
        return value.Value;
    }
}

Оператор явного преобразования типов возвращает значение внутреннего поля value. В случае, если значение Nullable-переменной не было присвоено, оператор возвращает значение по умолчанию внутреннего поля структуры ( value ). Пример:

int? i1 = null;
int i2 = ( int )i1; // явное преобразование из int? в int

Console.WriteLine( i2 );
// Вывод: 0

Оператор неявного преобразования из значимого типа T в тип Nullable<T> приводит к созданию нового экземпляра Nullable<T>. Следующий код:

int? i1 = null;
i1 = 5; // неявное преобразование из int в int? 

фактически эквивалентен следующему:

Nullable<int> i1 = new Nullable<int>();
i1 = new Nullable<int>( 5 );