.NET спецификация говорит о том, что приложение никогда не должно генерировать NullReferenceException. Впрочем, риск встречи подобного все равно остается во многих библиотеках и приложениях. Де-факто, NullReferenceException – это наиболее часто встречаемый тип исключений. И здесь на сцену выходит C# 8. В новой версии сего прекрасного языка ссылочные типы больше не могут принимать null по-умолчанию. Это и огромный плюс, и отличное нововведение. Но… Это все, конечно, хорошо, но как будут обстоять дела с поддержкой старых библиотек?
Именно в этой статье мы как раз и разберем этот вопрос.
Зачем нам вообще избегать NullReferenceException?
Когда выбрасывается NullReferenceException, причину ошибки далеко не всегда так уж просто найти. Ошибки обычно возникают далеко от очага реальной проблемы. Именно поэтому возникновение подобных ошибок и является крайне нежелательным. Потому вместо проверки на null-исключения просто выбрасывайте ArgumentNullException. Если где-то мы передаем null в качестве аргумента, мы можем просто на уровне компиляции запретить это делать. Просто выбрасываем ArgumentNullException – и мы сразу увидим первопричину ошибки в системе.
Давайте рассмотрим, как именно C# 8 решает подобные проблемы.
Установка C# 8
На момент написания статьи официального релиза C# 8 еще не было. Впрочем, даже сейчас вы можете его опробовать. Сейчас, на момент написания статьи, для этого нужно иметь Visual Studio 2017 15.5-15.7.
На заметку! Устанавливая эту версию компилятора, вы наверняка встретите множество предупреждений со стороны уже существующих C#-проектов. По-умолчанию используется последняя стабильная версия языка. Чтобы избавиться от предупреждений, просто явно задайте версию компилятора.
Ссылочные типы больше не могут принимать null
Ничего сложного для понимания здесь нет. Синтаксис, подобный обычным значимым типам. Хотите, чтобы ссылочный тип принимал null? Ставим после оглашения типа знак вопроса.
В то же время, хотя внешне синтаксис ссылочных и значимых типов выглядит похоже, сам принцип реализации кардинально другой.
- При работе со значимыми типами компилятор использует специальный тип Nullable. Это значимый тип, который помимо прочего также содержит в себе приватное булевское поле, определяющее, является ли значение переменной null.
- Со ссылочными типами компилятор просто добавляет атрибут Nullable. Версия 8 распознает этот атрибут и обрабатывает его соответствующим образом. Версия 7 его не понимает и просто игнорирует.
При компиляции программы под C# 7 Book b и Book? b будут распознаны одинаково.
Приведенный выше тип Book определяет не-nullable свойства Title и Publisher, а также nullable Isbn. Плюс, этот тип также содержит конструктор-кортеж. Используя тип Book и получая значение переменной Isbn, мы можем хранить полученные данные только в переменной типа string?.
Присваивание Nullable к не-Nullable
В случае, если нам нужно присвоить nullable-тип, C# 8 анализирует код. В коде ниже, так как isbn сравнивается с null, после условной конструкции isbn больше не сможет вернуть null. И так как сигнатура метода не предусматривает возвращение string?, при возвращении значения типа произойдет конверсия.
Конечно, эту же конструкцию можно написать гораздо проще и элегантнее.
Возвращение и передача значения
Здесь мы можем видеть класс NewAndGlory, построенный с использованием возможностей последней версии С#. Сигнатура метода GetANullString предусматривает возвращение null, так что в нашем случае этот метод просто возвращает null. Метод GetAString не сможет в свою очередь вернуть null. Что же касательно последнего метода PassAString, тут тоже все очень просто. Мы передаем string и возвращаем также string. По этой причине смысла в проверке на null нет.
С другой стороны, предположим, что у нас есть библиотека TheOldLib, использующая 7 компилятор (задается в файле *.csproj). Класс Legacy определяет метод GetANullString, что просто возвращает null. Метод PassAString принимает строку и проверяет ее на null. Также библиотека определяет интерфейс ILegacyInterface, задающий сигнатуру метода, что возвращает строку. С использованием шарпа 7 версии, мы не можем здесь указать, должна ли строка принимать null, или нет.
Відео курси за схожою тематикою:
Приложение на C# 8 могут использовать библиотеки, созданные и при помощи C# 7
Теперь давайте рассмотрим пример консольного приложения, что ссылается на старые и на новые библиотеки. Используя класс NewAndGlory, в качестве ожидаемого результата метода GetNullString мы можем получить только string?. Попытка же передать null в метод PassAString породит ошибку уровня компиляции (невозможно преобразовать null в не-nullable значение).
Обращаясь к классу Legacy, где метод GetANullString, результат может быть записан в тип string. И, так как эта библиотека не создавалась под эгидой C# 8, наш компилятор будет покорно молчать. Претензии он будет предъявлять только в отношении «новых» сборок. Также здесь мы можем вызвать метод PassAString и спокойно передать в нее null. Если бы компилятор ругался на все подобные нюансы более ранних сборок, список возможных ошибок мог формироваться до бесконечности, поэтому здесь и применяется принцип «разностного отношения».
Метод Foo интерфейса ILegacyInterface, определенный в библиотеке, собранной с использованием более ранней версии языка, – и здесь он возвращает string. Но как же нам тогда его использовать в C# 8? Как можно заметь ниже, здесь интерфейс может быть реализован с использованием как string, так и string?.
Интерфейсы, реализуемые в рамках C# 8, требуют прямого указания поведения по отношению null.
Приложения под C# 7 с использованием сборок C# 8
Что же касательно использования более новых версий сборок в ранних версиях языка, тут нет никаких проблем: все происходит, как и с любыми другими .NET-сборками. Приложение не будет видеть никаких string? – все nullable-ссылочные типы будут интерпретироваться как обычные ссылочные типы - в нашем случае просто как string. И, конечно же, проблема NullReferenceException остается.
Передача в метод PassAString null вызовет NullReferenceException. Для отлавливания подобного в рамках C# ранних версий мы можем проводить ручную проверку на null и выбрасывать ArgumentNullException. Возможно, эта ситуация по отношению к более старым версиям языка в миксе с новыми сборками с дальнейшим развитием C# 8 изменится, но это уже другой вопрос.
Безкоштовні вебінари за схожою тематикою:
В заключение
Ссылочные типы, не принимающие null, – это одна из ключевых возможностей С# 8, позволяющая минимизировать риск возникновения ошибок типа NullReferenceException. Подобное стало возможным благодаря изменениям внутренней реализации ссылочных типов языка. Впрочем, несмотря на все нововведения, C# 8 по-прежнему может без каких- либо проблем использовать более ранние библиотеки, как и более ранние версии языка – новые библиотеки. Microsoft осталась верной своим канонам обратной совместимости и технически это стало возможно благодаря использованию специальных атрибутов для nullable-типов.
Автор перевода: Евгений Лукашук
Статті за схожою тематикою