Что такое 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#:
Продвинутое руководство по nullable reference types
Одно из самых больших изменений в C# 8 — это nullable reference types. Ранее Андрей Дятлов (JetBrains) рассказал на конференции DotNext о трудностях и проблемах, которые вы можете встретить при работе с ними. Доклад понравился зрителям, поэтому теперь для Хабра готова его текстовая версия.
Наиболее полезным пост будет для тех, кто планирует использовать nullable reference types в больших проектах, которые невозможно перевести на использование NRT и проаннотировать целиком за короткое время; проектах, в которых используются собственные решения для ассертов или исключений, либо методы со сложными контрактами, связывающими наличие null во входных и выходных значениях, так как эти методы придется аннотировать для корректной работы компилятора с ними.
Я оставляю ссылку на оригинальный доклад. Дальше повествование пойдет от лица Андрея Дятлова, а пока что последний момент от меня: мы уже вовсю готовим осенний DotNext, и до 16 августа включительно принимаем заявки на доклады, так что если вам тоже есть о чем поведать дотнетчикам, откликайтесь.
Я занимаюсь поддержкой C# c 2015 года. В основном пишу анализаторы кода, занимаюсь рефакторингом и поддержкой новых версий языка. А по совместительству нахожу еще и баги в Roslyn.
План доклада
Что такое nullable reference types?
Но с reference-типами у вас не было такой подсказки, и если вам приходилось модифицировать этот класс, например, добавить в него метод получения инициалов сотрудника (по первым буквам имени и фамилии), то у вас возникали вопросы. А может ли фамилия сотрудника быть незаполненной?
Отличие от Nullable
Интересное сравнение реализаций null safety в C# и Kotlin в докладе Kotlin и С#. Чему языки могут поучиться друг у друга?(по ссылке тайм-код) Дмитрия Иванова.
Преимущества аннотаций
Скорее всего, кто-нибудь уже использует аннотации для решения проблем с null reference. Вы можете проаннотировать поля и коллекции при помощи атрибутов. А в чем тогда преимущество новой фичи языка?
Кроме того, вы не могли использовать атрибут там, где используются локальные переменные, где вы реализуете интерфейс или наследуетесь c какого-то класса, с ограничениями какого-то типа параметров. То есть во многих местах языка атрибуты в принципе запрещено писать, поэтому это нельзя было сделать. А новый синтаксис можно использовать везде, где вы можете в принципе написать тип. Это вcё, что я хотел сказать про саму фичу языка.
Это очень кратко, но команда компилятора, по их собственным оценкам, потратила на реализацию примерно 15 человеко-лет и, конечно, они подумали о том, с какими сложностями вы можете столкнуться и как это добавить в существующие проекты, в которых уже есть достаточно много кода, и он написан по-старому.
Включаем и пользуемся!
Для начала рассмотрим проблемы, с которыми вы можете столкнуться, если просто включите эту фичу для своего проекта.
Предупреждения компилятора без лишних усилий
Наверное, многие знают библиотеку NewtonsoftJson. Ее перевели на nullable reference types, и для этого пришлось изменить 170 файлов и 4000 строк кода. Если ваш проект немного больше, то скорее всего вам будет еще тяжелее.
Что можно сделать, если вы не хотите сейчас переписывать весь свой проект и тратить на это несколько месяцев или лет? Команда компилятора предусмотрела следующие опции: если вы хотите включить анализ только для части проекта, а для какой-то — нет, если вы хотите, чтобы вот прямо сейчас компилятор вам помог с проблемами в вашем коде, но вы пока вообще ничего не готовы для этого делать, ни одной аннотации проставлять.
Если вы пишете библиотеку, то можете захотеть проаннотировать ее только для пользователей, но пока не пользоваться этой фичей. Это тоже можно. Сейчас я расскажу подробнее о том, как всё это работает.
Во-первых, если у вас есть банковский софт, вы создаете транзакции, у вас есть клиент, информацию о котором нужно заполнить, вы проверяете, в какой стране живет клиент, требуется ли информация о почтовом индексе.
Продолжаем аннотировать
Допустим, у вас есть в компании еще один отдел, который пользуется теми же интерфейсами, которые вы пишете. И у вас есть информация о клиенте, которой мы только что пользовались.
Здесь я указал, что у клиента мы всегда спрашиваем имя и фамилию, а отчество он может не заполнять. Адреса у него может не быть, телефон он обязан оставить, а имейл — как пожелает.
Аналогичным образом это происходит, если вы используете в проекте dynamic — это всё тоже компилируется в атрибуты и скорее всего, вам это тоже не требуется знать, потому что это важно только для разработчиков компилятора.
Как они тогда компилируются? Дело в том, что они вообще не компилируются, так как компилятор уже выдал вам о них предупреждения. И после этого их снаружи использовать нельзя, поэтому проблемы как бы и нет, ничего не изменилось вообще в результирующем коде.
Теперь мы готовы
Поэтому вы можете при помощи препроцессора #nullable disable выключить в этой части вашего проекта и анализы, и аннотации. В результате оно скомпилируется без каких-либо аннотаций в качестве атрибутов. Люди, которые будут подключать эту библиотеку к своему проекту, будут знать о том, что эта часть контракта не проаннотирована.
Работаем с обобщенным кодом
Дальше расскажу о том, как это работает с обобщенным кодом. Но перед этим нужно понять, какие теперь есть отношения между nullable reference types и обычными.
В данном случае это nullable-строка. Аналогично это будет работать в любом месте, где компилятору требуется вывести какой-то общий тип для нескольких переменных.
Например, если у вас есть какой-нибудь дженерик-метод, и вы в один и тот же тип параметров передаете аргумент с разными аннотациями. Кроме того, если вы получили oblivious-строки (напоминаю, что это строки из не проаннотированных контекстов). Например, из подключенной библиотеки, где автор не предоставил аннотации. Либо из той части проекта, где вы анализ выключили, но будучи проаннотированными, они будут автоматически конвертироваться в not-nullable строки.
Но это с простыми типами, а как быть с дженериками? Если у вас есть последовательность not-nullable строк и последовательность nullable-строк, то с точки зрения системы типов вы можете положить not-nullable строки в nullable, но не наоборот.
И у нас получилась теперь интересная ситуация, когда nullable-типы находятся с разных сторон. К вопросу о том, что я объяснял ранее относительно того, какой тип является подтипом другого.
Изменения с выводом типов
Новые ограничения для параметров типа
Кроме того, теперь с nullable-reference типами вы можете добавлять generic constraints, аннотируя их. А старые constraints теперь означают, что сюда нельзя подставлять nullable-reference типы. Только старые типы, не допускающие null значений, которые не нужно проверять.
Аннотируем свой фреймворк
Например, у нас будет метод, который получает коллекцию элементов с constraint на IKeyOwner и пытается найти элемент по ключу. Если получилось — возвращает его. Если нет — то возвращает дефолтное значение.
Дело в том, что если мы попытаемся так написать, то компилятор скажет нам, что он не знает, что значит этот синтаксис. Дело в том, что интерфейс IKeyOwner можно реализовать как структурой, так и классом. А синтаксис с вопросом на конце должен означать для них разные вещи.
Как быть? Компилятор в своей подсказке предлагает нам добавить constraint class. Давайте попробуем это сделать.
Что произошло? Во-первых, теперь мы не можем пользоваться этими структурами, но возможно, они у вас в проекте не используются, и вам это не очень важно.
К сожалению, с классами тоже не всё до конца работает, потому что мы задали constraint на not-nullable классы, а хотели бы пользоваться любыми. Если есть массив, в котором лежат nullable-значения, то мы не можем использовать этот метод, чтобы найти в нем элемент. Может быть, нам поможет новый constraint class?
Неужели вы думаете, что сигнатуру FirstOrDefault теперь писать нельзя, а язык сломан? Думаете, что можно? А почему тогда этот пример не работает?
Кстати, а как это будет работать со структурами? Они станут теперь Nullable или нет? На самом деле, всё останется по-старому, потому что этот атрибут действительно просто добавляет аннотацию, если может добавить.
То есть для структур по-прежнему это точно not-nullable значение, всё работает по-старому, а с nullable reference-типами, если вы примените атрибут к возвращаемому значению метода, то компилятор продвинет его до nullable-версии типа, если это возможно. Последним штрихом у нас еще где-то есть Assert в программе, давайте добавим его в этот метод.
Опять что-то пошло не так. Дело в том, что теперь мы объявили input как not-nullable параметр, проверили при помощи ассерта, что он действительно not-null, а компилятор требует проверить еще раз.
Атрибуты-помощники
В System.Diagnostics.CodeAnalysis довольно много атрибутов, которые в основном помогают компилятору понять, что хотел сказать автор.
Если есть дженерик-код, который нельзя правильно проаннотировать, либо если есть какие-то сложные контракты. Когда в общем случае метод должен работать по одному сценарию, но в некоторых условиях может вернуть странное значение.
Кроме того, у вас есть группа атрибутов, которые говорят, что в некоторых случаях метод нормально не завершается — это [NotNull] атрибут. Не нужно путать его с JetBrains annotations Not Null, он называется так же, но лежит в другом namespace и в другой сборке и означает совершенно другое. Он означает, что если вы передали null в какой-то параметр, то метод выбросит исключение.
И последнее, что хотелось бы сказать про эти атрибуты, — это просто подсказка компилятору, что должен делать метод. Компилятор будет полагаться на эти аннотации при анализе использования метода.
Во-первых, скорее всего, вам это потребуется только с каким-то дженерик-кодом в вашем фреймворке, который обладает странным контрактом, и в повседневной жизни вам не придется непрерывно искать, как проаннотировать ваш метод. Скорее всего, это потребуется сделать один раз при переводе общей кодовой базы вашего проекта. Кроме того, компилятор не будет проверять, что этот метод соответствует тому, что вы проаннотировали, в отличие от code-контрактов. Ни в местах вызова, ни в самом методе ни рантайм-проверок, ни compile time-проверок не будет. Компилятор просто верит вам на слово, и если вы написали контракт для метода, а потом его не придерживаетесь, то сами виноваты.
А что, если компилятор все равно не прав?
Это новый синтаксис, который неформально называется dammit-оператор, и он используется для того, чтобы в каком-нибудь выражении просто убрать предупреждения компилятора, потому что каждый раз, когда анализ ошибается, писать #pragma warnings disable не очень удобно. Если компилятор в каком-то месте выдал неправильную ошибку, то вы можете либо игнорировать предупреждение, либо добавить ассерт, либо использовать dammit-оператор.
Этот оператор также позволяет инициализировать non-nullable переменные null ами, но так лучше не делать. Если у вас есть такое место, то потом вернитесь к нему, инициализируйте корректно. Если вы ее не инициализировали, компилятор узнает об этом и разрешит вам ей пользоваться, если нет — что-то пошло не так.
Стоит заметить, что dammit-оператор никогда не добавляет рантайм-проверок, и он просто убирает предупреждение в одном месте.
Кроме того, с dammit-оператором есть еще одна проблема, которую мы рассмотрим на следующем примере.
У нас есть код, который работает с неуспешными транзакциями.
А второй метод возвращает список транзакций на взятие комиссии, которые созданы нашей системой, а не пользователями.
И у нас есть таск, который будет получать словарь и как-то с ним работать: докидывать в пользовательские транзакции наши и пытаться что-то с ними сделать.
Однако здесь есть предупреждение. Мы получили словарь, а словарь мог вернуться как null. Пока всё хорошо. В этом коде есть второе предупреждение о том, что мы потом пытаемся в этот словарь добавить null. Словарь должен быть обозначен как содержащий в качестве значений nullable-типы. Метод, который позволяет нам получить транзакции для пользователя, возвращает not-nullable типы.
Я уже рассказывал на примере с list, почему такая конверсия запрещена. Конкретно здесь проблемы нет, проблема возникала из-за того, что мы получали две ссылки, типизированные по-разному. Здесь же мы получаем ссылку, просто меняем ей тип, старую ссылку выбрасываем. Остается просто словарь, можем положить в него null, можем из него null прочитать.
Проблемы на самом деле нет, чтобы решить вопрос с предупреждением, можем переложить из этого словаря в другой значение. Но кажется, это не имеет смысла, потому что мы просто потратим время, память. Давайте задавим предупреждение, потому что знаем, что проблемы нет. Добавим восклицательный знак. К сожалению, в этом методе произошло еще одно изменение, которое сейчас видно на слайде, но в реальном проекте это могут быть десятки строк ниже, где-то за границей экрана.
Несмотря на то, что мы объявили метод, который получает пользовательские транзакции, как метод, возвращающий null вместо словаря, мы объявили переменную как переменную, которая содержит null. Компилятор всё еще не предупреждает нас о том, что мы ей пользуемся.
Это случилось потому, что помимо того, чтобы задавить все предупреждения, восклицательный знак также говорит компилятору, что конкретно это значение не является null. К сожалению, об этом нужно помнить. Если вы будете пытаться бороться со сложными предупреждениями, то вам нужно это учитывать, потому что в противном случае вы просто можете быть уверены, что здесь null не бывает, потому что никаких предупреждений нет. Компилятор вас защищает. На самом деле, вы изменили семантику тем, что убрали предупреждение в другом месте.
А что если компилятор слишком прав?
Иногда компилятор ошибается в другую сторону и выдает вам слишком правильный тип.
Это пример от Дэвида Кина, разработчика Visual Studio из Microsoft. И с его слов: «Кажется, компилятор игнорирует меня, когда я пытаюсь ему сказать, что это nullable-строка».
Первое, что замечаем, — компилятор отмечает, что это правильное поведение, это действительно by design.
Но как тогда быть в исходном примере? У нас есть предупреждение, его там быть не должно. Если мы подняли его до ошибки, то у нас не компилируется проект.
Первое, что мы можем сделать, это использовать все тот же dammit-оператор для того, чтобы убрать здесь ошибку. Всё будет компилироваться, и кажется, что проблемы нет.
Кроме того, можно явно указать тип аргумента и сказать, что это не просто task from result, который должен вывести свой тип аргумента из переданного значения, а task from result от nullable-строк. Тогда тоже всё будет работать. Но, возможно, это будет длинно для сложных дженерик-вызовов.
И последнее, что вы можете сделать, это кастануть значение, получаемое из метода, к nullable версии того же типа. Почему, в отличие от переменной, это работает?
Дело в том, что компилятор не знает, зачем вы объявили переменную как nullable. Может быть, потом вы когда-нибудь будете класть в нее null, получив его из другого метода. Когда вы делаете cast, компилятор знает, что это конкретно про то значение, которое получили. Вы хотите в этом месте трактовать его как nullable-значение, и вы уже и переменную можете неявно типизировать и использовать, и всё тоже будет работать.
Итак, если компилятор не справляется с вашим кодом, то чтобы донести свою мысль до компилятора, вы можете либо использовать assert, либо dammit-оператор, если компилятор говорит, что где-то бывает null, а вы как программист знаете, что здесь это невозможно.
Вы можете, когда в обратную сторону вам нужен nullable-тип для вывода типов, либо сделать каст, либо указать явные типы-аргументы.
Наконец, если у вас есть более сложное предупреждение, например, преобразование сложных дженерик-типов со словарями, то вы можете использовать dammit-оператор. Главное — убедиться в том, что вы случайно не сказали компилятору сделать больше, чем нужно, и никаких побочных эффектов не случилось.
Помимо этого вы можете пользоваться препроцессором pragma warnings disable, nullable disable warnings для того, чтобы убрать предупреждения в конкретном регионе кода.
Где нужно быть особенно внимательным?
Существуют традиционно сложные места для data flow-анализа:
Рассмотрим подробнее каждый из этих случаев.
Использование не проаннотированных библиотек
Возьмем библиотеку NLog.
Кстати, если мы посмотрим на статистику скачивания пакетов с NuGet.org, то обнаружим, что среди топ-20 пакетов, не включая зависимые пакеты, например, xUnit, проаннотирован лишь Newtonsoft.json (на момент 2019 года)
На момент выхода статьи (август 2021) ситуация изменилась. Подробнее о состоянии на декабрь 2020 в докладе Джареда Парсонса Nullability in C#. Доклад не расшифрован, но в видео по ссылке есть русские субтитры.
Возможно, это связано с тем, что например, xUnit аннотировать не то чтобы сильно надо. Он практически всегда отдает вам not null, принимает nulls и тоже работает с этим. Какие-то библиотеки пока слишком масштабные, чтобы проаннотировать их за несколько месяцев с тех пор, как вышел C# 8. Надеюсь, в скором это изменится к лучшему.
Инициализация массивов
Кроме того, существуют места, которые команда компилятора просто не смогла поддержать.
Кросс-процедурные зависимости
Еще одна проблема — это кросс-процедурные зависимости. Например, если у вас есть кусок программы, который отвечает за генерацию контента, к примеру презентации.
Подобное поведение является балансом между количеством ложноположительных предупреждений и точностью анализа. Команда компилятора выбрала считать, что instance-методы никогда не изменяют значение полей, и если вы бы хотели в таких ситуациях получать предупреждение, вам стоит воспользоваться другим анализатором, например ReSharper или PVS-Studio.
Ref/in/out параметры
Кроме того есть еще ref / in / out параметры. Они могут измениться в любой момент, независимо от того, что делает метод, потому что это ссылка.
Если у вас есть аргумент check, который принимает not-nullable reference, компилятор будет проверять только то, что значение, которое ему передали по ссылке прямо сейчас, соответствует тому, что хочет метод.
Мы проверили, что в field не лежит null, передали его по ссылке в этот метод. Метод, например, заполнил, сбросил field и прочитал это же значение по ссылке через свой аргумент, и опять же не случилось предупреждений, в рантайме всё упало.
Замыкания
Компилятор не может выяснить, когда вы будете вызывать лямбду и что онабудет делать в этот момент.
Поэтому если в лямбде вы инициализируете замкнутое nullable-значение и вызовете ее, компилятор об этом не узнает, поэтому все равно будет выдавать предупреждение. В данном случае это безвредно, так как это просто предупреждение, которое можно убрать, например, поставив проверку.
Однако это работает и в обратном случае. Если вы проверили переменную, в лямбде значение сбросили на null и после этого пытаетесь воспользоваться этой переменной, предупреждения вы не получите, но исключение в рантайме будет.
Наконец, анализ может работать некорректно и в теле лямбда-функции с замыканием. Компилятор предполагает, что единственное место, где лямбда может быть выполнена — это там, где вы ее объявили.
Если вы в ней замкнулись на переменную, потом прочитали ее в лямбде, сбросили на null и вызвали тот делегат, то компилятор вам ничем не поможет.
С локальными функциями у команды компилятора есть планы как минимум добавить поддержку инициализаторов полей в конструкторах, когда они выполнены как локальные функции.
Аннотация массивов
Еще одно интересное место, в котором вам нужно быть внимательными, — это аннотация массивов. Дело в том, что при чтении из массивов в C#, итераторы всегда указывались в том порядке, в котором вы их объявляли, например:
Чтобы закрепить это, предлагаю пройти небольшой quiz:
При этом объявляться такой массив будет так:
Как такое могло случиться?
Дело в том что изначально, когда у нас был C# 7.3, nullable-типы были запрещены в паттерн-матчинге из-за того, что компилятор не учитывает, как расставлены пробелы, и не может понять, что вы имели в виду. Вы имели в виду, что это паттерн-матчинг с nullable-переменной, для которой он должен создать переменную, а затем — двоеточие, либо, что это conditional expression?:
В каком порядке это парсить? Nullable-типы были запрещены.
Потом, когда появились nullable reference-типы, они также были запрещены в type checks и паттерн-матчинге по тем же причинам.
Если вам интересно, как такое случилось и какие именно breaking changes были обнаружены со старым синтаксисом, вы можете пройти по ссылке, где была эта дискуссия.
Warning as errors
Напоследок я расскажу про warning as errors. Я часто встречаю такое мнение: «Сейчас мы поднимем предупреждения до ошибок, компилятор подскажет нам все места, где может быть NullReferenceException и у нас будет всё прекрасно: проверять ничего не надо, тестировать ничего не надо, всё всегда будет хорошо».
Вы действительно будете быстрее обнаруживать ошибки при изменениях в коде. Также вы сможете сразу заметить изменения контрактов в сторонних библиотеках при их изменениях, так как у вас просто перестанет компилироваться проект.
Но ваш код станет слишком хрупким, и при рефакторингах будут появляться ошибки. Иногда вы не сможете объявить переменную того типа, который хотели бы, например, для out var переменных. И так как вы больше не можете просто игнорировать некоторые предупреждения, ваш код может наполниться бессмысленными проверками, где анализ не справляется.
Вот несколько примеров таких проблем.
Первый пример отметила команда ASP.NET, когда они достаточно рано начали использовать nullable reference-типы, в том числе подняли их до ошибок. Суть примера сводилась к тому, что у них было две переменные, и они их попарно проверяли. Сначала обе null, потом обе — not null, потом первая not null.
Это пример того, как легко код с nullability и warnings as errors ломается рефакторингом. К примеру в какой-то момент метод M1 стал слишком большой и вы решили разбить его следующим образом:
Есть пример, когда человек в конструкторе задал значение, в соседнем методе пытается его использовать. В данном случае человек использует опцию warning source errors, поэтому его код не компилируется. Это происходит потому, что свойство item groups, которое он пытается прочитать, отмечено как nullable, и компилятор в методах не знает ни что было задано в конструкторе, ни могло ли это что-то измениться по мере исполнения программы. Отмечено как not nullable — будь добр проверить.
Dammit-оператор нельзя использовать везде в языке. Его можно использовать, чтобы убрать предупреждения из каких-то выражений, но не с объявления переменных. Например, если у вас есть dictionary try get value, вы пытаетесь получить из него string, то поскольку он отмечен TryGetValue, пройдет ли get value в случае, если он вернул false, положит в эту переменную null, то вы должны также переменную объявить как nullable. В этом месте вы даже не можете убрать предупреждение при помощи dammit-оператора.
Официальная рекомендация команды Microsoft — пользуйтесь старым синтаксисом C# 5, объявите переменную где-нибудь еще, тогда сможете воспользоваться dammit-оператором. Помимо этого в паттерн-матчинге я рассказывал на примере с массивами, почему так случилось, что nullable reference-типы там нельзя использовать. Поэтому все переменные с паттерн-матчингом у вас будут not nullable кроме var-переменных. Положить в них null потом будет нельзя. Вы можете либо пользоваться dammit-операторами, либо объявить вторую переменную, которая будет с тем типом, который вам нужен.
Выводы
Nullable reference-типы — большая фича, вы действительно будете получать больше информации о коде, если вы перейдете.
Вы сразу увидите, какая часть вашей программы допускает null, а какая — нет. Вы сможете найти больше ошибок на этапе разработки.
Но анализ может ошибаться. Если старый анализ Roslyn действительно сигнализировал о критических проблемах в коде, то либо вы забыли удалить переменную, либо использовали что-то другое в вашем коде. Анализ не может быть точным.
Если вы переводите большой проект на nullable reference-типы, рекомендую вам это сделать, потому что вы потратите меньше времени на поиск мест, в каких бывает null, а в каких — нет. Можно сделать это постепенно при помощи null-директив. Вы можете аннотировать фреймворк, вам понадобится это для сложных контрактов при помощи System.Diagnostics.CodeAnalysis-атрибутов.
Warnings as errors может сделать ваш код слишком хрупким.
Следующий DotNext пройдет онлайн с 21 по 22 октября. Билеты уже в продаже (и постепенно дорожают), программа на сайте появится позже. А если вам есть что поведать дотнетчикам, сейчас последняя возможность лично оказаться в этой программе: принимаем заявки на доклады до понедельника 16-го включительно.