Для чего используют nullable тип
Nullable Структура
Определение
Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
Параметры типа
Примеры
В следующем примере кода определяются три строки таблицы в образце базы данных Microsoft Pubs. Таблица содержит два столбца, которые не допускают значение null, и два столбца, допускающие значение null.
Комментарии
Nullable Структура поддерживает использование только типа значения в качестве типа, допускающего значение null, так как ссылочные типы допускают значение null при проектировании.
NullableКласс предоставляет дополнительную поддержку для Nullable структуры. NullableКласс поддерживает получение базового типа типа, допускающего значение null, а также операции сравнения и равенства с парами типов, допускающих значение null, базовый тип которых не поддерживает универсальные операции сравнения и равенства.
Фундаментальные свойства
Упаковка–преобразование и распаковка–преобразование
Конструкторы
Инициализирует новый экземпляр структуры Nullable заданным значением.
Свойства
Возвращает значение, указывающее, имеет ли текущий объект Nullable допустимое значение своего базового типа.
Методы
Указывает, равен ли текущий объект Nullable указанному объекту.
Извлекает хэш-код объекта, возвращенного свойством Value.
Извлекает значение текущего объекта Nullable или значение базового типа этого объекта по умолчанию.
Извлекает значение текущего объекта Nullable или заданное значение по умолчанию.
Операторы
Определяет явное преобразование экземпляра Nullable в его базовое значение.
Ссылочные типы, допускающие значение NULL (справочник по C#)
В этой статье рассматриваются ссылочные типы, допускающие значение NULL. Вы также можете объявить типы значений, допускающие значение NULL.
Ссылочные типы, допускающие значение NULL, доступны начиная с C# 8.0, в коде, который дал явное согласие на контекст, поддерживающий значение NULL. Ссылочные типы, допускающие значение NULL, предупреждения о значении NULL при статическом анализе и оператор, опускающий NULL, являются необязательными функциями языка. По умолчанию все они отключены. Контекст, допускающий значение NULL, контролируется на уровне проекта с помощью параметров сборки или в коде с помощью директив pragma.
В контексте, поддерживающем значение NULL:
Переменные notNull и nullable представлены типом String. Так как типы, допускающие и не допускающие значение NULL, хранятся в виде одного типа, существует несколько мест, где использование ссылочного типа, допускающего значение NULL, не допускается. Как правило, ссылочный тип, допускающий значение NULL, запрещено использовать в качестве базового класса или реализованного интерфейса. Ссылочный тип, допускающий значение NULL, не может использоваться в выражении проверки типа или создания объекта. Ссылочный тип, допускающий значение NULL, не может быть типом выражения доступа к члену. Эти конструкции показаны в следующих примерах:
Ссылки, допускающие значение NULL, и статический анализ
Примеры в предыдущем разделе иллюстрируют природу ссылочных типов, допускающих значение NULL. Ссылочные типы, допускающие значение NULL, не являются новыми типами классов, а обозначены заметками для существующих ссылочных типов. Компилятор использует эти заметки, чтобы помочь найти потенциальные ошибки для пустых ссылок в коде. Во время выполнения нет никакой разницы между ссылочным типом, не допускающим значение NULL, и ссылочным типом, допускающим значение NULL. Компилятор не добавляет никакую проверку для ссылочных типов, не допускающих значение NULL, во время выполнения. Преимущества заключаются в анализе времени компиляции. Компилятор создает предупреждения, помогающие находить и исправлять потенциальные ошибки со значениями NULL в коде. Вы объявляете свое намерение, и компилятор предупреждает вас, если код нарушает его.
В контексте, допускающем значение NULL, компилятор выполняет статический анализ для переменных любого ссылочного типа, как допускающего, так и не допускающего значение NULL. Компилятор отслеживает состояние NULL каждой ссылочной переменной в виде не равно NULL или может быть NULL. Состоянием по умолчанию для ссылки, не допускающей значение NULL, является не равно NULL. Состоянием по умолчанию для ссылки, допускающей значение NULL, является может быть NULL.
Ссылочные типы, не допускающие значение NULL, всегда должны быть безопасными для разыменования, так как их состоянием NULL является не равно NULL. Чтобы применить это правило, компилятор выдает предупреждения, если ссылочный тип, не допускающий значение NULL, не инициализируется со значением, отличным от NULL. Локальные переменные должны присваиваться там же, где они объявляются. Каждому полю должно быть присвоено значение, не равное NULL, в инициализаторе поля или в каждом конструкторе. Компилятор выдает предупреждения, если ссылка, не допускающая значение NULL, присваивается ссылке с состоянием может быть NULL. В целом, так как ссылка, не допускающая значение NULL, имеет состояние не равно NULL, при разыменовании этих переменных предупреждения не выдаются.
При назначении выражения с состоянием может быть NULL для ссылочного типа, не допускающего значения NULL, компилятор создает предупреждения. Компилятор будет создавать предупреждения для этой переменной до тех пор, пока она не будет назначена выражению со значением не равно NULL.
В следующем фрагменте кода показано, где компилятор выдает предупреждения при использовании этого класса:
Задание контекста, допускающего значение NULL
Спецификация языка C#
Дополнительные сведения см. в следующих предложениях для спецификации языка C#:
Справочник по C#. Типы значений, допускающие значение NULL
В C# 8.0 появилась возможность использования ссылочных типов, допускающих значение NULL. Дополнительные сведения см. в статье Ссылочные типы, допускающие значение NULL. Типы значений, допускающие значение NULL, доступны начиная с C# 2.
Назначение и объявление
Проверка экземпляра типа значения, допускающего значение NULL
Вы всегда можете использовать следующие свойства только для чтения, чтобы проверить и получить значение переменной типа, допускающего значение NULL:
Преобразование из типа значения, допускающего значение NULL, в базовый тип
Вы можете также явно привести тип значения, допускающий значение NULL, к типу, не допускающему значение NULL, как показано в примере ниже.
Операторы с нулификацией
Если между двумя типами данных определено пользовательское преобразование типов, то это же преобразование можно также использовать между соответствующими типами, допускающими значение NULL.
Упаковка-преобразование и распаковка-преобразование
Экземпляр типа значения, допускающего значение NULL, T? упакован следующим образом:
Идентификация типа значений, допускающего значение NULL
В следующем примере показано, как определить, представляет ли экземпляр System.Type сконструированный тип значений, допускающий значение NULL, т. е. тип System.Nullable с указанным параметром типа T :
Как показано в примере, при помощи оператора typeof можно создать экземпляр System.Type.
Если вы хотите определить, принадлежит ли экземпляр к типу значений, допускающему значение NULL, не используйте метод Object.GetType для получения экземпляра Type для тестирования с помощью приведенного выше кода. При вызове метода Object.GetType в экземпляре типа значений, допускающего значение NULL, экземпляр упаковывается в Object. Так как упаковка экземпляра типа значений, допускающего значение NULL, значение которого отлично от NULL, эквивалентна упаковке значения базового типа, GetType возвращается экземпляр Type, представляющий базовый тип типа значений, допускающего значение NULL:
Кроме того, не используйте оператор is, чтобы определить, принадлежит ли экземпляр к типу значений, допускающему значение NULL. Как показано в следующем примере, вы не можете отличить типы экземпляра типа значений, допускающего значение NULL, и его экземпляра базового типа с помощью оператора is :
Чтобы определить, принадлежит ли экземпляр типу значений, допускающему значение NULL, можно использовать код, представленный в следующем примере:
Методы, описанные в этом разделе, неприменимы в случае ссылочных типов, допускающих значения NULL.
Спецификация языка C#
Дополнительные сведения см. в следующих разделах статьи Спецификация языка C#:
Хорошо ли вы помните nullable value types? Заглядываем «под капот»
В последнее время модной темой стали nullable reference types. Однако старые добрые nullable value types никуда не делись и всё так же активно используются. Хорошо ли вы помните нюансы работы с ними? Предлагаю освежить или проверить свои знания, ознакомившись с этой статьёй. Примеры кода на C# и IL, обращения к спецификации CLI и коду CoreCLR прилагаются. Начать предлагаю с интересной задачки.
Примечание. Если вас интересуют nullable reference types, можете познакомиться с несколькими статьями моих коллег: «Nullable Reference типы в C# 8.0 и статический анализ», «Nullable Reference не защищают, и вот доказательства».
Посмотрите на пример кода ниже, и ответьте, что будет выведено в консоль. И, что не менее важно, почему. Только давайте сразу договоримся, что вы ответите, как есть: без подсказок компиляторов, документации, вычитывания литературы или чего-то подобного. 🙂
Что ж, давайте немного порассуждаем. Возьмём несколько основных направлений мысли, которые, как мне кажется, могут возникнуть.
1. Исходим из того, что int? — ссылочный тип.
Давайте рассуждать так, что int? — это ссылочный тип. В таком случае в а будет записано значение null, оно же будет записано и в aObj после присвоения. В b будет записана ссылка на какой-то объект. Она также будет записана и в bObj после присвоения. В итоге, Object.ReferenceEquals примет в качестве аргументов значение null и ненулевую ссылку на объект, так что…
Всё очевидно, ответ — False!
2. Исходим из того, что int? — значимый тип.
А может быть вы сомневаетесь, что int? — ссылочный тип? И вы уверены в этом, несмотря на выражение int? a = null? Что ж, давайте зайдём с другой стороны и будем отталкиваться от того, что int? — значимый тип.
В таком случае выражение int? a = null выглядит немного странно, но предположим, что это опять в C# сахара сверху насыпали. Получается, что a хранит какой-то объект. b тоже хранит какой-то объект. При инициализации переменных aObj и bObj будет произведена упаковка объектов, хранимых в a и b, в результате чего в aObj и в bObj будут записаны разные ссылки. Получается, что Object.ReferenceEquals в качестве аргументов принимает ссылки на разные объекты, следовательно…
Всё очевидно, ответ — False!
Всё очевидно, ответ — False!
Для тех, кто отталкивался от значимых типов, — если у вас вдруг закрались какие-то сомнения про сравнение ссылок, то можно посмотреть документацию по Object.ReferenceEquals на docs.microsoft.com. В частности, там тоже затрагивают тему значимых типов и упаковки/распаковки. Правда, там описывается кейс, когда экземпляры значимых типов передаются непосредственно в метод, мы же упаковку вынесли отдельно, но суть та же.
When comparing value types. If objA and objB are value types, they are boxed before they are passed to the ReferenceEquals method. This means that if both objA and objB represent the same instance of a value type, the ReferenceEquals method nevertheless returns false, as the following example shows.
Казалось бы, здесь статью можно и закончить, вот только… правильный ответ — True.
Что ж, давайте разбираться.
Разбираемся
Есть два пути — простой и интересный.
Простой путь
Интересный путь
На этой тропинке нам будет недостаточно документации. Она описывает поведение, но не отвечает на вопрос ‘почему’?
Что такое на самом деле int? и null в соответствующем контексте? Почему это работает так? В IL коде используются разные команды или нет? Отличается поведение на уровне CLR? Ещё какая-то магия?
Начнём с разбора сущности int?, чтобы вспомнить основы, и постепенно дойдём до разбора первоначального кейса. Так как C# — язык достаточно «приторный», периодически будем обращаться к IL коду, чтобы смотреть в суть вещей (да, документация по C# — не наш путь сегодня).
int?, Nullable
Здесь рассмотрим основы nullable value types в принципе (что из себя представляет, во что компилируются в IL и т.п.). Ответ на вопрос из задания рассмотрен в следующем разделе.
Рассмотрим фрагмент кода.
Несмотря на то, что на C# инициализация этих переменных выглядит по-разному, для всех них будет сгенерирован один и тот же IL код.
Как видно, в C# всё от души сдобрено синтаксическим сахаром, чтобы нам с вами жилось лучше, по факту же:
С инициализацией по умолчанию мы разобрались — соответствующий IL код мы видели выше. Что же происходит здесь, когда мы хотим проинициализировать aVal значением 62?
Взглянем на IL код:
Опять же, ничего сложного — на evaluation stack загружается адрес aVal, а также значение 62, после чего вызывается конструктор с сигнатурой Nullable (T). То есть два следующих выражения будут полностью идентичны:
В этом же можно убедиться, опять взглянув на IL код:
А что же касается проверок? Например, что на самом деле представляет из себя код следующего вида?
Правильно, для понимания вновь обратимся к соответствующему IL коду.
Для удобства работы тип Nullable определяет:
Упаковка Nullable
Напомню, что при упаковке объекта значимого типа в куче будет создан новый объект. Это поведение наглядно иллюстрирует следующий фрагмент кода:
Результатом сравнения ссылок ожидаемо будет false, так как произошло 2 операции упаковки и создание двух объектов, ссылки на которые были записаны в obj1 и obj2.
Результат всё также ожидаем — false.
А теперь вместо 62 прописываем дефолтное значение.
Иии… результатом неожиданно становится true. Казалось бы, имеем всё те же 2 операции упаковки, создание двух объектов и ссылки на два разных объекта, но результат-то — true!
Ага, наверняка опять дело в сахаре, и что-то поменялось на уровне IL кода! Давайте посмотрим.
Как мы видим, везде упаковка происходит идентичным образом — значения локальных переменных загружается на evaluation stack (инструкция ldloc), после чего происходит сама упаковка за счёт вызова команды box, для которой указывается, какой, собственно, тип будем упаковывать.
Обращаемся к спецификации Common Language Infrastructure, смотрим описание команды box и находим интересное примечание касаемо nullable типов:
If typeTok is a value type, the box instruction converts val to its boxed form.… If it is a nullable type, this is done by inspecting val’s HasValue property; if it is false, a null reference is pushed onto the stack; otherwise, the result of boxing val’s Value property is pushed onto the stack.
Отсюда следует несколько выводов, расставляющих точки над ‘i’:
Состояние экземпляра перед упаковкой:
Состояние экземпляра перед упаковкой:
В исходном примере, который был в самом начале статьи, происходит точно то же самое:
Ради интереса заглянем в исходный код CoreCLR из упомянутого ранее репозитория dotnet/runtime. Нас интересует файл object.cpp, конкретно — метод Nullable::Box, который и содержит нужную нам логику:
Здесь всё то, о чём мы говорили выше. Если не храним значение — возвращаем NULL:
Иначе производим упаковку:
Заключение
Ради интереса предлагаю показать пример из начала статьи своим коллегам и друзьям. Смогут ли они дать верный ответ и обосновать его? Если нет, приглашайте их познакомиться со статьёй. Если же смогут — что ж, моё уважение!
Надеюсь, это было небольшое, но увлекательное приключение. 🙂
P.S. У кого-то мог возникнуть вопрос: а с чего вообще началось погружение в эту тему? Мы делали новое диагностическое правило в PVS-Studio на тему того, что Object.ReferenceEquals работает с аргументами, один из которых представлен значимым типом. Вдруг оказалось, что с Nullable есть неожиданный момент в поведении при упаковке. Посмотрели IL код — box как box. Посмотрели спецификацию CLI — ага, вот оно! Показалось, что это достаточно интересный кейс, про который стоит рассказать — раз! — и статья перед вами.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. Check how you remember nullable value types. Let’s peek under the hood.
Ссылочные типы, допускающие значение null
До C# 8.0 все ссылочные типы допускали значение NULL. Ссылочные типы, допускающие значение NULL, относятся к группе функций, появившихся в C# 8.0, которые можно использовать, чтобы снизить вероятность возникновения исключения System.NullReferenceException в среде выполнения. Ссылочные типы, допускающие значения NULL, содержат три функции, которые помогают избежать возникновения этих исключений, а также возможность явно помечать ссылочный тип как допускающий значение NULL.
Анализ состояния NULL
Рассмотрим следующий пример.
В предыдущем примере компилятор определяет, что переменная message maybe-null при выводе первого сообщения. Для второго сообщения предупреждение не выводится. Последняя строка кода выдает предупреждение, поскольку originalMessage может иметь значение NULL. В следующем примере показано более практичное использование для обхода дерева узлов до корня с обработкой каждого узла во время обхода:
В C# 10 добавлен ряд улучшений для определенного присваивания и анализа состояния NULL. При обновлении до C# 10 сократится число ложноположительных результатов с предупреждением о NULL. Дополнительные сведения об улучшениях см. в статье о спецификации функций для определенного присваивания.
Атрибуты в сигнатурах API
Анализ состояния NULL требует указаний от разработчиков для понимания семантики API. Некоторые API обеспечивают проверку значений NULL и должны изменять состояние NULL переменной с maybe-null на not-null. Другие API возвращают выражения, которые являются not-null или maybe-null в зависимости от состояния NULL входных аргументов. Для примера рассмотрим следующий код, в котором отображается сообщение:
Аннотации для переменных, допускающих значения NULL
Используйте аннотации, которые могут объявлять, является ли переменная ссылочным типом, допускающим значение NULL, или ссылочным типом, не допускающим значения NULL. Эти аннотации указывают на состояние NULL для переменных:
Универсальные шаблоны
Можно указать другое поведение с помощью ограничений.
Контексты допустимости значения NULL
При включении в существующей базе кода новые функции, которые защищают от исключения System.NullReferenceException, могут нарушать работу:
Для использования этих функций их необходимо явно включить в существующих проектах. Это позволяет предоставить путь миграции и сохранить обратную совместимость. Контексты допустимости значения NULL детально контролируют, как компилятор интерпретирует переменные ссылочного типа. Контекст аннотаций, допускающий значение NULL, определяет поведение компилятора. Существует четыре значения для контекста аннотаций, допускающего значение NULL:
Контекст аннотаций о допустимости значения NULL и контекст с предупреждениями о допустимости значения NULL можно задать для проекта с помощью элемента в файле CSPROJ. Этот элемент настраивает, как компилятор интерпретирует допустимость значений NULL для типов и какие предупреждения выдаются. В следующей таблице показаны допустимые значения и приводится сводка по задаваемым контекстам.
Переменные ссылочного типа в коде, скомпилированном в версиях до C# 8, или в отключенном контексте, имеют свойство nullable-oblivious. Можно назначить литерал null или переменную может быть NULL переменной, которая имеет свойство nullable oblivious. Однако по умолчанию для переменной со свойством nullable oblivious установлено состояние не равно NULL.
Вы можете выбрать оптимальный параметр для своего проекта:
Пример:
Также можно использовать директивы для задания этих контекстов в любом месте в исходном коде. Они наиболее полезны при переносе большой базы кода.
Для любой строки кода можно задать любое из следующих сочетаний:
Контекст предупреждения | Контекст аннотаций | Использование |
---|---|---|
проект по умолчанию | проект по умолчанию | По умолчанию |
Включено | disabled | Исправление предупреждения при анализе |
Включено | проект по умолчанию | Исправление предупреждения при анализе |
проект по умолчанию | Включено | Добавление аннотации для типа |
Включено | Включено | Код уже перенесен |
disabled | Включено | Добавление аннотации к коду перед исправлением предупреждений |
disabled | disabled | Добавление устаревшего кода в перенесенный проект |
проект по умолчанию | disabled | Редко |
disabled | проект по умолчанию | Редко |
Эти девять сочетаний позволяют точно контролировать диагностику, выдаваемую компилятором для кода. Вы можете включить дополнительные компоненты в любой обновляемой области, не получая дополнительных предупреждений, которые вы еще не можете устранить.
Глобальный контекст, допускающий значения NULL, не применяется для созданных файлов кода. В любом случае контекст, допускающий значение NULL, отключен для любого исходного файла, помеченного как созданный. Это означает, что все интерфейсы API в создаваемых файлах не заносятся в заметки. Существует четыре способа пометки файла как созданного:
Эти параметры предоставляют две отдельные стратегии для обновления существующей базы кода так, чтобы она могла использовать ссылочные типы, допускающие значение NULL.
Известные ошибки
Массивы и структуры, содержащие ссылочные типы, являются известными проблемами в ссылках, допускающих значения NULL, и статическом анализе, который определяет безопасность значения NULL. В обоих случаях ссылка, не допускающая значения NULL, может быть инициализирована как null без предупреждений.
Структуры
Структуре, которая содержит ссылочные типы, не допускающие значения NULL, может быть присвоено значение default без предупреждения. Рассмотрим следующий пример.
Еще один более распространенный случай связан с работой с универсальными структурами. Рассмотрим следующий пример.
В предыдущем примере свойство Bar во время выполнения будет иметь значение null и присваивается строке, не допускающей значения NULL. При этом предупреждения не возвращаются.
Массивы
При использовании ссылочных типов, допускающих значения NULL, также могут возникать известные ошибки, связанные с массивами. Рассмотрим следующий пример, в котором не выдаются предупреждения: