Null, великий и ужасный
Именно так и никак иначе: null в C# — однозначно ошибочное решение, бездумно скопированное из более ранних языков.
- Самое страшное: в качестве значения любого ссылочного типа может использоваться универсальный предатель — null, на которого никак не среагирует компилятор. Зато во время исполнения легко получить нож в спину — NullReferenceException. Обрабатывать это исключение бесполезно: оно означает безусловную ошибку в коде.
- Перец на рану: сбой (NRE при попытке разыменования) может находится очень далеко от дефекта (использование null там, где ждут полноценный объект).
- Упитанный пушной зверек: null неизлечим — никакие будущие нововведения в платформе и языке не избавят нас от прокаженного унаследованного кода, который физически невозможно перестать использовать.
Этот ящик Пандоры был открыт еще при создании языка ALGOL W великим Хоаром, который позднее назвал собственную идею ошибкой на миллиард долларов.
Лучшая историческая альтернатива
Разумеется, она была, причем очевидная по современным меркам
- Унифицированный Nullable для значимых и ссылочных типов.
- Разыменование Nullable только через специальные операторы (тернарный — ?:, Элвиса — ?., coalesce — ?), предусматривающие обязательную обработку обоих вариантов (наличие или отсутствие объекта) без выбрасывания исключений.
Самое трагичное, что все это не было откровением и даже новинкой уже к моменту проектирования первой версии языка. Увы, тогда матерых функциональщиков в команде Хейлсберга не было.
Лекарства для текущей реальности
Хотя прогноз очень серьезный, летального исхода можно избежать за счет применения различных практик и инструментов. Способы и их особенности пронумерованы для удобства ссылок.
Явные проверки на null в операторе if. Очень прямолинейный способ с массой серьезных недостатков.
- Гигантская масса шумового кода, единственное назначение которого — выбросить исключение поближе к месту предательства.
- Основной сценарий, загроможденный проверками, читается плохо
- Требуемую проверку легко пропустить или полениться написать
- Проверки можно добавлять отнюдь не везде (например, это нельзя сделать для автосвойств)
- Проверки не бесплатны во время выполнения.
Атрибут NotNull. Немного упрощает использование явных проверок
- Позволяет использовать статический анализ
- Поддерживается R#
- Требует добавления изрядного количества скорее вредного, чем бесполезного кода: в львиной доле вариантов использования null недопустим, а значит атрибут придется добавлять буквально везде.
Паттерн проектирования Null object. Очень хороший способ, но с ограниченной сферой применения.
- Позволяет не использовать проверок на null там, где существует эквивалент нуля в виде объекта: пустой IEnumerable, пустой массив, пустая строка, ордер с нулевой суммой и т.п. Самое впечатляющее применение — автоматическая реализация интерфейсов в мок-библиотеках.
- Бесполезен в остальных ситуация: как только вам потребовалось отличать в коде нулевой объект от остальных — вы имеете эквивалент null вместо null object, что является уже двойным предательством: неполноценный объект, который даже NRE не выбрасывает.
Конвенция о возврате живых объектов по умолчанию. Очень просто и эффективно.
Любой метод или свойство, для которых явно не заявлена возможность возвращать null, должны всегда предоставлять полноценный объект. Для поддержания достаточно выработки хорошей привычки, например, посредством ревью кода.
- Разработчики сторонних библиотек ничего про ваше соглашение не знают
- Нарушения соглашения выявить непросто.
Конвенция о стандартных способах явно указать что свойство или метод может вернуть null: например, префикс Try или суффикс OrDefault в имени метода. Органичное дополнение к возврату полноценных объектов по умолчанию. Достоинства и недостатки те же.
Атрибут CanBeNull. Добрый антипод-близнец атрибута NotNull.
- Поддерживается R#
- Позволяет помечать явно опасные места, вместо массовой разметки по площадям как NotNull
- Неудобен в случае когда null возвращается часто.
Операторы C# (тернарный, Элвиса, coalesce)
- Позволяют элегантно и лаконично организовать проверку и обработку null значений без потери прозрачности основного сценария обработки.
- Практически не упрощают выброс ArgumentException при передаче null в качестве значения NotNull параметра.
- Покрывают лишь некоторую часть вариантов использования.
- Остальные недостатки те же, что и у проверок в лоб.
Тип Optional. Позволяет явно поддержать отсутствие объекта.
- Можно полностью исключить NRE
- Можно гарантировать наличие обработки обоих основных вариантов на этапе компиляции.
- Против легаси этот вариант немного помогает, вернее, помогает немного.
- Во время исполнения помимо дополнительных инструкций добавляется еще и memory traffic
Монада Maybe. LINQ для удобной обработки случаев как наличия, так и отсутствия объекта.
- Сочетает элегантность кода с полнотой покрытия вариантов использования.
- В сочетании с типом Optional дает кумулятивный эффект.
- Отладка затруднена, так как с точки зрения отладчика вся цепочка вызовов является одной строкой.
- Легаси по-прежнему остается ахиллесовой пятой.
Программирование по контракту.
- В теории почти идеал, на практике все гораздо печальнее.
- Библиотека Code Contracts скорее мертва, чем жива.
- Очень сильное замедление сборки, вплоть до невозможности использовать в цикле редактирование-компиляция-отладка.
Пакет Fody/NullGuard. Автоматические проверки на null на стероидах.
- Проверяется все: передача параметров, запись, чтение и возврат значений, даже автосвойста.
- Никакого оверхеда в исходном коде
- Никаких случайных пропусков проверок
- Поддержка атрибута AllowNull — с одной стороны это очень хорошо, а с другой — аналогичный атрибут у решарпера другой.
- С библиотеками, агрессивно использующими null, требуется довольно много ручной работы по добавлению атрибутов AllowNull
- Поддержка отключения проверки для отдельных классов и целых сборок
- Используется вплетение кода после компиляции, но время сборки растет умеренно.
- Сами проверки работают только во время выполнения.
- Гарантируется выброс исключения максимально близко к дефекту (возврату null туда, где ожидается реальный объект).
- Тотальность проверок помогает даже при работе с легаси, позволяя как можно быстрее обнаружить, пометить и обезвредить даже null, полученный из чужого кода.
- Если отсутствие объекта допустимо — NullGuard сможет помочь только при попытках передать его куда не следует.
- Вычистив дефекты в тестовой версии, можно собрать промышленную из тех же исходников с отключенными проверками, получив нулевую стоимость во время выполнения при гарантии сохранения всей прочей логики.
Ссылочные типы без возможности присвоения null (если добавят в одну из будущих версий C#)
- Проверки во время компиляции.
- Можно полностью ликвидировать NRE в новом коде.
- В реальности не реализовано, надеюсь, что только пока
- Единообразия со значимыми типами не будет.
- Легаси достанет и здесь.
Итоги
Буду краток — все выводы в таблице:
Настоятельная рекомендация | Антипаттерн | На ваш вкус и потребности |
---|---|---|
4, 5, 7, 11, 12 (когда и если будет реализовано) | 1, 2 | 3, 6, 8, 9, 10 |
На предвосхищение ООП через 20 лет не претендую, но дополнениям и критике буду очень рад.
Комментарии (9)
12 сентября 2016 в 04:21
0↑
↓
Можно перейти на F# и… начать лепить вместо null везде Unchecked.defaultof<'T>12 сентября 2016 в 05:06
0↑
↓
Бедный этот null. Добавьте в статью опрос, как часто вы имеете NullReferenceException? Лично вот я — не чаще раза в неделю, а то и реже, использую только ?:,?… и ?… Я конечно, стараюсь null не возвращать и не передавать, но это на уровне смысла методов скорее, чем претензия к языку.Проблема не в том что разыменование бросает исключение. Есть и другие исключения, тоже летят. Выход за границы массива например. А представьте, нету null. Тогда все как в анекдоте про буратино и яблоки. Добавлять метод IsValid? Те же проверки же. По-моему суть вопроса не в наличии маркера (отсутствия чего-либо) null, а в том что языки программирования и компиляторы различают объекты от ссылок. Переменная на то и переменная, что она может быть изменена.
12 сентября 2016 в 06:26
+1↑
↓
Если нет null обычно есть что-то другое, например Maybe, гораздо хуже когда null есть, а операторов из п.7 нет.12 сентября 2016 в 08:20
0↑
↓
Ну и получаем (с Maybe) тот же if null, только обернутый.Моя мысль скорее в том, что языки из 90-х (и их наследники) оперируют в императивном стиле, и, следовательно, есть переменные. C# не исключение, и ждать от него обязательств что у переменной всегда должен быть объект… зачем? Там выше про F# сказали. Давно уже хотел присмотреться к нему как следует, но всё времени нет. Теперь его приоритет увеличил. Может следующий проект на нём буду :) Если Xamarin F#-ready (а это вроде так).
12 сентября 2016 в 05:20 (комментарий был изменён)
0↑
↓
https://github.com/dotnet/roslyn/issues/7445
https://github.com/dotnet/roslyn/blob/features/NullableReferenceTypes/docs/features/NullableReferenceTypes/Nullable reference types.md12 сентября 2016 в 08:50
0↑
↓
Интересно, а можно ли было ссылочные типы объединить с optional? То есть, иными словами можно ли на уровне языка любой ссылочный тип рассматривать как optional у которого null — значение none?
Или может быть можно сформулировать иначе: возможно ли (опять на уровне языка) сделать optional таким, чтобы любой ссылочный тип рассматривался как optional, но при этом была возможность явно указать что данный ссылочный тип не может иметь значения null (атрибут NotNull?)12 сентября 2016 в 08:54
+2↑
↓
Хайп вокруг заменителей null напоминает страшный социальный эксперимент, в котором 9 подставных человек называют белое черным, чтобы смутить десятого. Потому что, я бы ещё понял null-aware работу с обычным указателем, без которой компилятор вас изобьет. Но попытка убедить людей в том, что какой-то особый монадический контейнер с точно такой же повсеместной проверкой на IfPresent (или как-то иначе) засоряющий клиентский код это намного лучше — это просто какой-то артхаусный фильм про 'мир сошёл с ума'. Что там, кстати, с NULL в sql, не надумали отказаться? Пусть реальный мир подвинется, в ИТ не может не быть значения, пусть даже это значение НетЗначения.12 сентября 2016 в 09:30
0↑
↓
Да тут все проще. Сам null — вполне нормальное явление. Но некоторые типы нуллабельны по умолчанию, некоторые — ненуллабельны. И получается так, что это свойство типа неявное, оно как-бы спрятано внутри самого типа. То есть когда вы имеете дело с типом T, вы заранее не знаете, может ли он быть null, или нет.
А если бы например при разработке языка ввели правило, что T — это всегда ненуллабельный тип, а скажем T? — всегда нуллабельный, вероятно ясности было бы больше.
12 сентября 2016 в 09:58
0↑
↓
Мне одному кажется что проблема излишне раздута в статье?Все (по крайней мере должны) проверяют входные параметры в функцию и выходные из функции. Работа кода, который делает что-то с пустым объектом обычно бесполезна. Что толку, если вы получили пустой объект? Логика работы программы всё равно нарушена.
Да и возврат не null, а пустого объекта тоже имеет место быть.