Атрибуция: Это выросло из связанного вопроса P.SE
Мой опыт работы в C / C ++, но я много работал на Java и в настоящее время пишу на C #. Из-за моего C-фона проверка пройденных и возвращенных указателей является подержанной, но я признаю, что это искажает мою точку зрения.
Недавно я увидел упоминание об объекте Null Object Pattern, где идея заключается в том, что объект всегда возвращается. Обычный регистр возвращает ожидаемый заполненный объект, а регистр ошибки возвращает пустой объект вместо нулевого указателя. Предполагается, что вызывающая функция всегда будет иметь какой-то объект для доступа и, следовательно, избегает нарушений памяти нулевого доступа.
- Так каковы плюсы / минусы нулевой проверки по сравнению с использованием шаблона нулевого объекта?
Я могу видеть более чистый код вызова с помощью NOP, но я также вижу, где он может создавать скрытые сбои, которые иначе не возникают. Я предпочел бы, чтобы мое приложение терпело неудачу (иначе исключение), пока я его разрабатывал, чем молчаливая ошибка, уходящая в дикую природу.
- Не может ли шаблон пустого объекта иметь проблемы, подобные тем, что он не выполняет проверку нуля?
Многие из объектов, с которыми я работал, содержат собственные объекты или контейнеры. Похоже, мне нужно иметь особый случай, чтобы гарантировать, что все контейнеры основного объекта имели свои собственные пустые объекты. Похоже, что это может стать уродливым с несколькими слоями вложенности.
источник
Ответы:
Вы не будете использовать шаблон нулевого объекта в местах, где возвращается нулевой (или нулевой объект), потому что произошел катастрофический сбой. В тех местах я бы продолжал возвращать ноль. В некоторых случаях, если нет восстановления, вы также можете аварийно завершить работу, потому что, по крайней мере, аварийный дамп будет точно указывать, где возникла проблема. В таких случаях, когда вы добавляете свою собственную обработку ошибок, вы все равно собираетесь завершить процесс (опять же, я сказал для случаев, когда нет восстановления), но ваша обработка ошибок будет маскировать очень важную информацию, которую мог бы предоставить аварийный дамп.
Null Object Pattern больше подходит для мест, где есть поведение по умолчанию, которое можно использовать в случае, когда объект не найден. Например, рассмотрим следующее:
Если вы используете NOP, вы должны написать:
Обратите внимание, что поведение этого кода «если Боб существует, я хочу обновить его адрес». Очевидно, что если ваше приложение требует присутствия Боба, вы не хотите молча преуспевать. Но есть случаи, когда этот тип поведения будет уместным. И в этих случаях разве NOP не производит намного более чистый и сжатый код?
В местах, где вы действительно не можете жить без Боба, я бы заставил GetUser () выдать исключение приложения (т.е. не нарушение прав доступа или что-то в этом роде), которое было бы обработано на более высоком уровне и сообщало бы об общем сбое операции. В этом случае нет необходимости в NOP, но также нет необходимости явно проверять NULL. IMO, эти проверки для NULL, только делают код больше и лишают читабельности. Проверка на NULL - все еще правильный выбор дизайна для некоторых интерфейсов, но не так много, как думают некоторые люди.
источник
Pros
Cons
Это последнее «про» является основным отличием (по моему опыту), когда каждый из них должен применяться. «Неудача должна быть шумной?». В некоторых случаях вы хотите, чтобы неудача была жесткой и немедленной; если какой-то сценарий, который никогда не должен произойти, каким-то образом Если жизненно важный ресурс не был найден ... и т. Д. В некоторых случаях у вас все в порядке с нормальным значением по умолчанию, поскольку на самом деле это не ошибка : получение значения из словаря, но ключ отсутствует, например.
Как и любое другое дизайнерское решение, в зависимости от ваших потребностей есть свои плюсы и минусы.
источник
Я не хороший источник объективной информации, но субъективно
В таких средах, как Java или C #, это не даст вам основного преимущества явности - безопасности завершенности решения, потому что вы все равно можете получить нулевое значение вместо любого объекта в системе, а у Java нет средств (кроме пользовательских аннотаций для Beans-компонентов). и т. д.), чтобы защитить вас от этого, кроме явных ifs. Но в системе, свободной от нулевых реализаций, подход к решению проблемы таким образом (с объектами, представляющими исключительные случаи) дает все упомянутые преимущества. Поэтому попробуйте прочитать о чем-то, кроме основных языков, и вы обнаружите, что меняете способы кодирования.
В целом, объекты Null / Error / возвращаемые типы считаются очень надежной стратегией обработки ошибок и вполне математически обоснованными, в то время как стратегия обработки исключений Java или C идет всего лишь на один шаг выше стратегии «die ASAP» - так что, в основном, все бремя нестабильность и непредсказуемость системы на разработчика или сопровождающего.
Есть также монады, которые можно считать превосходящими эту стратегию (сначала они также сложнее в понимании), но это больше касается случаев, когда вам нужно моделировать внешние аспекты типа, в то время как Null Object моделирует внутренние аспекты. Так что NonExistingUser, вероятно, лучше смоделировать как монаду может быть существующим.
И, наконец, я не думаю, что есть какая-то связь между шаблоном Null Object и тихой обработкой ситуаций ошибок. Это должно быть наоборот - это должно заставить вас явно моделировать каждый случай ошибки и обрабатывать его соответствующим образом. Теперь, конечно, вы можете решить быть неаккуратным (по какой-либо причине) и пропустить обработку ошибки в явном виде, но обрабатывать ее в общем - хорошо, это был ваш явный выбор.
источник
Я думаю, что интересно отметить, что жизнеспособность Null Object, кажется, немного отличается в зависимости от того, типизирован ли ваш язык или нет. Ruby - это язык, который реализует объект для Null (или
Nil
в терминологии языка).Вместо возврата указателя на неинициализированную память язык вернет объект
Nil
. Этому способствует тот факт, что язык динамически типизирован. Функция не всегда гарантирует возвратint
. Он может вернуться,Nil
если это то, что ему нужно сделать. Это становится более запутанным и трудным сделать это на языке статической типизации, потому что вы, естественно, ожидаете, что значение null будет применимо к любому объекту, который может быть вызван по ссылке. Вы должны начать реализацию нулевых версий любого объекта, который может быть нулевым (или пойти по пути Scala / Haskell и иметь какую-то оболочку «Maybe»).MrLister поднял вопрос о том, что в C ++ вам не гарантируется инициализация ссылки / указателя при первом его создании. Имея выделенный нулевой объект вместо необработанного нулевого указателя, вы можете получить некоторые приятные дополнительные функции. Ruby's
Nil
включает функциюto_s
(toString), которая приводит к пустому тексту. Это замечательно, если вы не возражаете получить нулевой возврат по строке и хотите пропустить сообщение об этом без сбоев. Открытые классы также позволяют вам вручную переопределить вывод,Nil
если это необходимо.Конечно, все это можно взять с зерном соли. Я считаю, что в динамическом языке нулевой объект может быть очень удобным при правильном использовании. К сожалению, чтобы получить максимальную отдачу от этого, вам может потребоваться отредактировать его базовую функциональность, что может затруднить понимание и поддержку вашей программы для людей, привыкших работать с ее более стандартными формами.
источник
Для некоторой дополнительной точки зрения взгляните на Objective-C, где вместо объекта Null значение nil вроде работает как объект. Любое сообщение, отправленное объекту nil, не имеет никакого эффекта и возвращает значение 0 / 0.0 / NO (false) / NULL / nil, независимо от типа возвращаемого значения метода. Это используется как очень распространенный шаблон в программировании на MacOS X и iOS, и обычно методы разрабатываются так, чтобы нулевой объект, возвращающий 0, был разумным результатом.
Например, если вы хотите проверить, содержит ли строка один или несколько символов, вы можете написать
и получить приемлемый результат, если aString == nil, потому что несуществующая строка не содержит символов. Людям, как правило, требуется некоторое время, чтобы привыкнуть к этому, но мне, вероятно, понадобится некоторое время, чтобы привыкнуть к проверке нулевых значений везде на других языках.
(Существует также одноэлементный объект класса NSNull, который ведет себя совершенно иначе и практически не используется на практике).
источник
nil
get#define
'd в NULL и ведет себя как нулевой объект. Это функция времени выполнения, в то время как в C # / Java нулевые указатели выдают ошибки.Не используйте либо, если вы можете избежать этого. Используйте фабричную функцию, возвращающую тип опции, кортеж или выделенный тип суммы. Другими словами, представляйте потенциально дефолтное значение с типом, отличным от гарантированного не подлежащего дефолту значения.
Во-первых, давайте перечислим desiderata, а затем подумаем, как мы можем выразить это на нескольких языках (C ++, OCaml, Python)
grep
может выследить возможные ошибки.Я думаю, что напряжение между шаблоном нулевого объекта и нулевыми указателями исходит из (5). Если мы сможем обнаружить ошибки достаточно рано, то (5) станет спорным.
Давайте рассмотрим этот язык по языкам:
C ++
На мой взгляд, класс C ++ обычно должен конструироваться по умолчанию, поскольку он облегчает взаимодействие с библиотеками и облегчает использование класса в контейнерах. Это также упрощает наследование, поскольку вам не нужно думать о том, какой конструктор суперкласса вызывать.
Однако это означает, что вы не можете точно знать, находится ли значение типа
MyClass
в «состоянии по умолчанию» или нет. Помимо того, что вы вставляетеbool nonempty
или аналогичное поле для отображения поверхностных настроек по умолчанию во время выполнения, лучшее, что вы можете сделать, - это создать новые экземплярыMyClass
таким образом, чтобы заставить пользователя проверить его .Я бы порекомендовал использовать фабричную функцию, которая возвращает
std::optional<MyClass>
,std::pair<bool, MyClass>
или,std::unique_ptr<MyClass>
если возможно, ссылку на r-значение .Если вы хотите, чтобы ваша фабричная функция возвращала какое-то состояние "заполнителя", отличное от созданного по умолчанию
MyClass
, используйте astd::pair
и обязательно задокументируйте, что ваша функция делает это.Если фабричная функция имеет уникальное имя, ее легко
grep
найти и искать неряшливость. Тем не менее, это трудноgrep
для случаев, когда программист должен был использовать заводскую функцию, но не использовал ее .OCaml
Если вы используете такой язык, как OCaml, то вы можете просто использовать
option
тип (в стандартной библиотеке OCaml) илиeither
тип (не в стандартной библиотеке, но легко развернуть свой собственный). Илиdefaultable
тип (я придумываю терминdefaultable
).A,
defaultable
как показано выше, лучше, чем пара, потому что пользователь должен сопоставить шаблон для извлечения'a
и не может просто игнорировать первый элемент пары.Эквивалент
defaultable
типа, показанного выше, можно использовать в C ++, используя astd::variant
с двумя экземплярами одного типа, но частиstd::variant
API непригодны, если оба типа одинаковы. Также это странное использование,std::variant
так как его конструкторы типов не называются.питон
Вы все равно не получаете никакой проверки во время компиляции для Python. Но, будучи динамически типизированным, обычно нет обстоятельств, когда вам нужен экземпляр заполнителя какого-либо типа, чтобы успокоить компилятор.
Я бы порекомендовал просто вызвать исключение, когда вам придется создать экземпляр по умолчанию.
Если это неприемлемо, я бы порекомендовал создать объект,
DefaultedMyClass
который наследуется,MyClass
и вернуть его из функции фабрики. Это дает вам гибкость в плане «заглушения» функциональности в дефолтных экземплярах, если вам нужно.источник