Функциональный C#: Ненулевые ссылочные типы

Эта статья третья в серии «Функциональный C#»:

  1. Функциональный C#: Неизменные объекты
  2. Функциональный C#: Одержимость примитивами
  3. Функциональный C#: Ненулевые ссылочные типы
  4. Функциональный C#: Обработка исключений

Ненулевые ссылочные типы в C#: введение

Посмотрите на пример, приведенный ниже:

Что-то знакомое, да? Но какие ошибки вы наблюдаете?

Проблема заключается в том, что мы не знаем наверняка, действительно ли метод GetById вернет ненулевой экземпляр. Несмотря ни на что, есть определенный шанс того, что мы получим null. В таком случае мы получим исключение NullReferenceException. Ситуация может быть еще хуже, если перед тем, как использовать переменную customer, она еще не будет инициализирована методом GetById. В этом случае нам будет очень сложно отловить такую ошибку.

Чем быстрее мы получаем обратную связь, тем меньше времени мы имеем для исправления ошибки. Конечно, максимально быстрый ответ мы можем получить только от компилятора. Согласитесь, было бы здорово написать наш код, и пусть компилятор сделает все необходимые проверки?

Где Customer! означает ненулевой тип, т.е. экземпляры такого класса не могут принимать значение null в принципе. Как думаете, было бы здорово быть уверенным в том, что компилятор укажет нам, если какой-либо кусок кода может вернуть null?

Безусловно, было бы круто! Или даже лучше:

То есть, сделать все ссылочные типы по умолчанию ненулевыми (так же, как и типы значений). И если мы захотим применить нулевой тип, то следует воспользоваться таким кодом:

Можете ли вы себе представить мир без всех этих раздражающих NullReferenceException? Я тоже не могу.

К сожалению, необнуляемые ссылочные типы не могут быть введены в C# как признак языка. Чтобы подробнее узнать об этой теме, рекомендую к прочтению эти статьи:

  1. Статья Эрика Липперта (Eric Lippert)
  2. Интересная, но практически нереализуемое «дизайнерское» решение

Но не волнуйтесь. Хоть мы и не можем заставить компилятор помочь нам и использовать мощности ненулевых ссылочных типов, есть и обходные пути, к которым сейчас и прибегнем. Давайте взглянем на класс Customer, который мы написали в прошлой статье:

Как вы могли заметить, мы перенесли поля класса Customer (имя и почту) в отдельные классы. Однако, мы ничего не можем поделать с проверками на null. Это единственные условия, которые остались в классе Customer.

Как избавиться от проверок на null

Итак, как мы можем избавиться от таких проверок? Конечно же при помощи IL рерайтинга (Intermediate Language Rewrite)!

Есть замечательный NuGet пакет под названием NullGuard.Fody. Для начала вам нужно скачать и установить пакет. Пометьте сборку таким атрибутом:

[assembly: NullGuard(ValidationFlags.All)]

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

Или даже еще проще:

Несмотря на визуальную простоту класса, на самом деле он выглядит вот так:

Но как быть со значением null?

Так как же мы можем определить то, что значение некоторого типа может быть пустым (null)? Для этого мы будем использовать монаду Maybe.

Как вы можете заметить, входные значения для класса Maybe отмечены атрибутом AllowNull. Теперь мы можем написать следующий код с использованием Maybe:

И теперь становится очевидным то, что метод GetById может вернуть нулевое значение. Кроме того, теперь вы не сможете случайно перепутать значение, которое может принимать null, с необнуляемым значением, что привело бы к ошибке компилятора.

Конечно же вы теперь должны решить, какие сборки будут обработаны пакетом NullGuard.Fody. Вероятно, применение данного пакета в WPF не лучшая идея, поскольку там имеется множество системных компонентов, которые являются по своей сути обнуляемыми. Именно поэтому добавление проверок на null не даст вам особого преимущества. Однако для всех остальных сборок данный метод будет более чем актуальным.

Небольшое замечание о монаде Maybe. Вы, возможно, захотите назвать ее Option из-за соглашения об именовании языка F#. Я лично предпочитаю использовать Maybe, но по-моему, распределение программистов в этом вопросе примерно 50 на 50. Конечно, это всего лишь дело вкуса.

Что насчет статических проверок?

Окей, быстрая обратная связь во время выполнения это хорошо, но это все еще обратная связь во время выполнения. Было бы замечательно анализировать код еще быстрее — скажем, на этапе компиляции.

Ответ кроется в атрибуте NotNull замечательного дополнения к Visual Studio — ReSharper. Применив атрибут NotNull к какому-то методу и вернув в нем нулевое значение, вы получите предупреждение от плагина ReSharper.

Данный способ может здорово облегчить вам жизнь, однако он страдает от некоторых проблем.

Во-первых, ReSharper работает по методу от обратного. Сейчас вам нужно отмечать атрибутом NotNull те значения, которые не могут быть нулевыми. Было бы гораздо удобнее помечать этим атрибутом значения, которые, наоборот, могли бы быть нулевыми. Остальные же по умолчанию считались бы необнуляемыми.

Во-вторых, предупреждения — это всего лишь предупреждения. Вы можете запросто не обратить на них внимание и пропустить их. Разумеется, мы можем установить настройки Visual Studio так, что он будет понимать предупреждения как ошибки. Однако с монадой Maybe у вас гораздо меньше шансов ошибиться.

Именно по этим причинам я не использую ReSharper, хотя они и очень полезны.

Вывод

Подход, описанный выше, действительно очень мощный:

  1. Вы быстро отловите баг с нулевым значением. Теперь вас не будут надоедать постоянные NullReferenceException.
  2. Вы увеличите читаемость кода. Теперь не будут везде мелькать постоянные проверки значения на null перед использованием объекта.
  3. По умолчанию все ваши методы будут защищены от исключения NullReferenceException. Причем вам не надо будет помечать каждый новый метод атрибутом NotNull.

А на этом все! В следующей статье мы мы рассмотрим вопрос обработки исключений в функционально C#.

Перевод статьи «Functional C#: Non-nullable reference types»