нульові вказівники проти нульової картини об’єкта


27

Атрибуція: Це випливало із пов'язаного з питанням P.SE

Моя передумова перебуває на C / C ++, але я працював неабияку кількість в Java і в даний час кодую C #. Через мій досвід C перевірка переданих та повернених покажчиків - це секонд-хенд, але я визнаю, що це змінює мою точку зору.

Нещодавно я бачив згадку про Null Object Pattern, де ідея полягає в тому, що об’єкт завжди повертається. Звичайний регістр повертає очікуваний, заселений об'єкт, а випадок помилки повертає порожній об'єкт замість нульового вказівника. Припущення полягає в тому, що функція виклику завжди матиме якийсь об’єкт для доступу, а отже, уникає порушень пам’яті з нульовим доступом.

  • Отже, які плюси та мінуси нульової перевірки порівняно з використанням Null Object Pattern?

Я можу побачити чистіший код виклику за допомогою NOP, але я також бачу, де це створило б приховані збої, які інакше не піднімаються. Я вважаю за краще, щоб моє додаток не працювало важко (він же був винятком), коли я його розробляю, ніж мовчазну помилку втекти в дику природу.

  • Чи не може у Null Object Pattern виникати подібні проблеми, як і не виконувати нульову перевірку?

Багато об'єктів, з якими я працював, містять власні об'єкти або контейнери. Здається, я мав би мати особливий випадок, щоб гарантувати, що всі контейнери основного об'єкта мають власні порожні об'єкти. Здається, що це може стати некрасивим через кілька шарів гніздування.


Не "випадок помилки", але "у всіх випадках дійсний об'єкт".

@ ThorbjørnRavnAndersen - хороший момент, і я відредагував питання, щоб це відобразити. Будь ласка, дайте мені знати, чи я все-таки помиляюсь з припущенням щодо НП.

6
Коротка відповідь полягає в тому, що "нульовий шаблон об'єкта" є неправильним. Це більш точно називається "нульовим об'єктом анти-шаблон". Зазвичай вам краще "об’єкт помилки" - дійсний об'єкт, який реалізує весь інтерфейс класу, але будь-яке його використання спричиняє гучну, раптову смерть.
Джеррі Труну

1
@JerryCoffin цей випадок повинен бути вказаний винятком. Ідея полягає в тому, що ви ніколи не можете отримати нуль і, отже, не потрібно перевіряти на null.

1
Мені не подобається цей "візерунок". Але, можливо, він укорінений у перенапружених реляційних базах даних, де легше посилатись на спеціальні "нульові рядки" в іноземних ключах під час постановки редагованих даних, перед остаточним оновленням, коли всі зовнішні ключі ідеально не є нульовими.

Відповіді:


24

Ви б не використовували Null Object Pattern у місцях повернення null (або Null Object), оскільки стався катастрофічний збій. У тих місцях я б продовжував повертати нульові. У деяких випадках, якщо відновлення не буде, ви також можете вийти з ладу, тому що принаймні дамп збою вкаже, де саме виникла проблема. У таких випадках, коли ви додаєте власне поводження з помилками, ви все одно збираєтеся вбити процес (знову ж таки, я сказав, що для випадків, коли не відновлено), але обробка помилок маскує дуже важливу інформацію, яку надав би дамп аварійного завершення.

Null Object Pattern більше призначений для місць, де є поведінка за замовчуванням, яку можна прийняти у випадку, коли об’єкт не знайдено. Наприклад, розглянемо наступне:

User* pUser = GetUser( "Bob" );

if( pUser )
{
    pUser->SetAddress( "123 Fake St." );
}

Якщо ви використовуєте NOP, ви напишете:

GetUser( "Bob" )->SetAddress( "123 Fake St." );

Зауважте, що поведінка цього коду - "якщо Боб існує, я хочу оновити його адресу". Очевидно, якщо у вашій заявці вимагається присутність Боба, ви не хочете мовчки домагатися успіху. Але є випадки, коли такий тип поведінки був би доречним. І в тих випадках, чи NOP не створює набагато більш чіткий і стислий код?

У місцях, де ви справді не можете жити без Боба, я б змусив GetUser () викинути виняток із програми (тобто не матиме доступу до порушення чи чогось подібного), який би оброблявся на більш високому рівні та повідомляв би про загальну помилку роботи. У цьому випадку немає необхідності в NOP, але також не потрібно чітко перевіряти наявність NULL. IMO, ці перевірки на NULL, лише збільшують код і позбавляють від читабельності. Перевірка на NULL все ще є правильним вибором дизайну для деяких інтерфейсів, але не майже стільки, скільки люди схильні думати.


4
Погодитись із випадком використання для нульових шаблонів об'єктів. Але навіщо повертати нульове значення, стикаючись з катастрофічними невдачами? Чому б не кинути винятки?
Апоорв Хурасія

2
@MonsterTruck: повернути це. Коли я пишу код, я обов'язково перевіряю більшість вхідних даних, отриманих від зовнішніх компонентів. Однак між внутрішніми класами, якщо я записую код таким, що функція одного класу ніколи не поверне NULL, то на стороні, що викликає, я не додаватиму нульову перевірку просто для "більш безпечного". Єдиний спосіб, коли це значення може бути NULL - це якщо у моїй програмі сталася якась катастрофічна логічна помилка. У такому випадку я хочу, щоб моя програма вийшла з ладу саме на тому місці, тому що тоді я отримую хороший дамп-файл аварії та повертаю назад стек та стан об'єкта для виявлення логічної помилки.
DXM

2
@MonsterTruck: маючи на увазі "перевернути це", я не мав явного повернення NULL, коли ви виявите катастрофічну помилку, але очікуйте, що NULL виникне лише через якусь непередбачувану катастрофічну помилку.
DXM

Тепер я розумію, що ви мали на увазі під катастрофічною помилкою - щось, що спричиняє збій самого виділення об'єкта. Правильно? Редагувати: Не можна робити @ DXM, тому що ваш псевдонім має лише 3 знаки.
Апоорв Хурасія

@MonsterTruck: дивно щодо речі "@". Я знаю, що раніше це робили інші. Цікаво, чи недавно вони переробили веб-сайт. Його не потрібно виділяти. Якщо я пишу клас і мета API - це завжди повертати дійсний об'єкт, я б не вимагав, щоб абонент робив нульову перевірку. Але катастрофічна помилка може бути будь-якою, як помилка програміста (помилка помилки, яка призвела до повернення NULL), або, можливо, деяка умова гонки, яка стирала об'єкт до його часу, або, можливо, якась пошкодження стека. По суті, будь-який несподіваний шлях коду, який призвів до повернення NULL. Коли це станеться, ви хочете ...
DXM

7

Отже, які плюси та мінуси нульової перевірки порівняно з використанням Null Object Pattern?

Плюси

  • Нульова перевірка краще, оскільки вона вирішує більше випадків. Не всі об’єкти мають нормальну поведінку за замовчуванням або не мають доступу.
  • Нульова перевірка є більш твердою. Навіть об'єкти із звичайними значеннями за замовчуванням використовуються в місцях, де стандартний стандарт недійсний. Код повинен вийти з ладу близько до першопричини, якщо він вийде з ладу. Код, очевидно, повинен вийти з ладу, якщо він вийде з ладу.

Мінуси

  • Звичайні параметри за замовчуванням зазвичай призводять до отримання більш чистого коду.
  • Звичайні параметри за замовчуванням зазвичай призводять до менш катастрофічних помилок, якщо їм вдасться потрапити в дику природу.

Цей останній "профі" є головним диференціатором (на мій досвід) щодо того, коли кожне слід застосовувати. "Чи повинен провал бути шумним?". У деяких випадках ви хочете, щоб невдача була важкою і негайною; якщо якийсь сценарій, який ніколи не повинен статися, так і є. Якщо життєвий ресурс не знайдено ... і т. Д. У деяких випадках ви добре з нормальним замовчуванням, оскільки це насправді не помилка : отримання значення зі словника, але ключ відсутній, наприклад.

Як і будь-яке інше дизайнерське рішення, в залежності від ваших потреб існують передові сторони і недоліки.


1
У розділі "Про": Погодьтеся з першим пунктом. Другий момент - вина дизайнерів, тому що навіть нуль можна використовувати там, де цього не повинно бути. Не говорить нічого поганого про шаблон просто відображається на розробнику, який неправильно використовував шаблон.
Апоорв Хурасія

Що є більш катастрофічним невдачею, що не в змозі надсилати гроші або надсилати (і, таким чином, більше не мати) грошей, не отримуючи їх і не знаючи їх перед явною перевіркою?
user470365

Мінуси: стандартні параметри за замовчуванням можна використовувати для створення нових об'єктів. Якщо повертається ненульовий об’єкт, деякі його атрибути можуть бути змінені під час створення іншого об'єкта. Якщо нульовий об'єкт повернуто, його можна використовувати для створення нового об'єкта. В обох випадках працює той самий код. Немає необхідності в окремому коді для копіювання-модифікації та нового. Це частина філософії, що кожен рядок коду - це інше місце для помилок; скорочення ліній зменшує помилки.
shawnhcorey

@shawnhcorey - що?
Теластин

Нульовий об'єкт повинен бути корисним, як би це був реальний об'єкт. Наприклад, розглянемо NaN IEEE (не число). Якщо рівняння піде не так, воно повертається. І його можна використовувати для подальших розрахунків, оскільки будь-яка операція на ньому поверне NaN. Це спрощує код, оскільки вам не доведеться перевіряти NaN після кожної арифметичної операції.
shawnhcorey

6

Я не є хорошим джерелом об'єктивної інформації, але суб'єктивно:

  • Я не хочу, щоб моя система вмирала ніколи; для мене це ознака погано розробленої або неповної системи; повна система повинна обробляти всі можливі випадки і ніколи не потрапляти в несподіваний стан; щоб досягти цього, мені потрібно бути чітким при моделюванні всіх потоків і ситуацій; використання нулів не є явним чином і групує безліч різних випадків під однією дупкою; Я віддаю перевагу об’єкту NonExistingUser перед null, я віддаю перевагу NaN перед null, ... такий підхід дозволяє мені бути явним щодо цих ситуацій та їхньої обробки з такою кількістю деталей, наскільки я хочу, в той час як null залишає мені лише параметр null; у такому середовищі, як Java, будь-який об’єкт може бути нульовим, то чому б ви вважали за краще ховати в цьому загальному пулі випадків також ваш конкретний випадок?
  • нульові реалізації мають різну поведінку, ніж об'єкт, і в основному приклеюються до набору всіх об'єктів (чому? чому? чому? чому?); для мене така різка різниця здається результатом проектування нулів вказівкою на помилку; в такому випадку система, швидше за все, загине (я гадаю, що багато людей насправді вважають за краще їх система вмирати у вищезгаданих постах), якщо явно не обробляється; для мене це хибний підхід у корені - він дозволяє системі загинути за замовчуванням, якщо ви явно не подбали про це, це не дуже безпечно, правда? але в будь-якому випадку неможливо написати код, який трактує значення null та об'єкт однаково - лише кілька основних вбудованих операторів (присвоєння, рівність, параметр тощо) будуть працювати, весь інший код просто вийде з ладу, і вам потрібно дві абсолютно різні стежки;
  • краще використовувати масштаби Null Object - ви можете помістити в нього стільки інформації та структури; NonExistingUser - дуже хороший приклад - він може містити електронну пошту користувача, якого ви намагалися отримати, і запропонувати створити нового користувача на основі цієї інформації, тоді як при нульовому рішенні мені потрібно було б подумати, як зберегти електронну пошту, до якої люди намагалися отримати доступ. близький до нульової обробки результатів;

У такому середовищі, як Java або C #, це не дасть вам основної переваги явності - безпека повноцінності рішення, тому що ви все одно можете отримати нульове місце на місці будь-якого об'єкта в системі, а Java не має засобів (крім спеціальних анотацій для Beans тощо), щоб захистити вас від цього, крім явних ifs. Але в системі, що не має нульової імплементації, підхід до вирішення проблеми таким чином (з об'єктами, що представляють виняткові випадки) дає всі згадані переваги. Тому спробуйте прочитати про щось інше, ніж основні мови, і ви виявите, що ви змінюєте способи кодування.

Взагалі об'єкти Null / Error / типи повернення вважаються дуже міцною стратегією обробки помилок і досить математично обгрунтованою, тоді як стратегія обробки винятків Java або C йде лише на один крок вище стратегії "die ASAP" - тому в основному залишайте весь тягар нестабільність і непередбачуваність системи на розробника або на сервісі.

Також є монади, які можна вважати вищими за цю стратегію (також вищі за складністю розуміння спочатку), але це більше про випадки, коли потрібно моделювати зовнішні аспекти типу, а Null Object моделює внутрішні аспекти. Тож Non-ExistingUser, ймовірно, краще моделюється як монада, можливо, існує.

І нарешті, я не вважаю, що між шаблоном Null Object і мовчазним поводженням з помилками існує якийсь зв’язок. Це повинно бути навпаки - це повинно змусити вас чітко моделювати кожен випадок помилки та обробляти це відповідно. Тепер, звичайно, ви можете вирішити бути неохайним (з будь-якої причини) і пропустити поводження з помилкою явно, але вирішити це загально - ну це був ваш явний вибір.


9
Причина, що мені подобається, що моя заявка не працює, коли трапляється щось несподіване, - це те, що сталося щось несподіване Як це сталося? Чому? У мене виходить дамп аварійного завершення роботи, і я можу розслідувати та виправити потенційно небезпечну проблему, а не дозволяти їй приховатись у коді. У C # я, звичайно, можу наздогнати всі несподівані винятки, щоб спробувати відновити програму, але навіть тоді я хочу зареєструвати місце, де сталася проблема, і з’ясувати, чи потрібне це окреме поводження (навіть якщо це "не зафіксуйте цю помилку, вона фактично очікувана і підлягає відновленню ").
Луань

3

Я думаю, що цікаво відзначити, що життєздатність Null Object, здається, трохи відрізняється в залежності від того, чи є ваша мова статично введеною. Ruby - це мова, яка реалізує об’єкт для Null (або Nilв термінології мови).

Замість повернення вказівника на неініціалізовану пам'ять мова поверне об'єкт Nil. Цьому сприяє той факт, що мова динамічно набирається. Функція не гарантована, щоб завжди повертати дані int. Він може повернутися, Nilякщо це потрібно зробити. Це стає складніше і важче зробити це статично набраною мовою, тому що ви, природно, очікуєте, що нуль може бути застосована до будь-якого об'єкта, який можна назвати посиланням. Вам доведеться почати реалізовувати нульові версії будь-якого об’єкта, який може бути нульовим (або пройти маршрут Scala / Haskell і мати якусь обгортку "Можливо").

MrLister підкреслив той факт, що в C ++ вам не гарантовано мати ініціалізацію посилання / покажчика при першому створенні. Маючи виділений нульовий об’єкт замість необробленого нульового вказівника, ви можете отримати кілька приємних додаткових функцій. Ruby's Nilвключає функцію to_s(toString), яка призводить до чистого тексту. Це чудово, якщо ви не заперечуєте отримати нульовий прибуток на рядку і хочете пропустити повідомлення про нього без збоїв. Відкриті класи також дозволяють вручну переосмислити результат, Nilякщо це буде потрібно.

Зрозуміло, це все можна взяти з зерном солі. Я вважаю, що в динамічній мові нульовий об’єкт може бути великою зручністю при правильному використанні. На жаль, для отримання максимальної користі від цього може знадобитися редагування його базової функціональності, що може зробити вашу програму важкою для розуміння та підтримки для людей, які звикли працювати з її більш стандартними формами.


1

Для отримання додаткової точки зору подивіться на Objective-C, де замість об'єкта Null значення nil kind-вида працює як об'єкт. Будь-яке повідомлення, надіслане об'єкту нуля, не має ефекту і повертає значення 0 / 0.0 / NO (хибне) / NULL / nil, незалежно від типу поверненого значення методу. Це використовується як дуже розповсюджена модель у програмах MacOS X та iOS, і зазвичай методи будуть розроблені так, що нульовий об'єкт, що повертає 0, є розумним результатом.

Наприклад, якщо ви хочете перевірити, чи містить рядок один чи більше символів, ви можете написати

aString.length > 0

і отримайте розумний результат, якщо aString == nil, оскільки неіснуюча рядок не містить жодних символів. Люди, як правило, потребують певного часу, щоб звикнути до цього, але, мабуть, знадобиться певний час, щоб звикнути перевіряти нульові значення скрізь іншими мовами.

(Є також однотонний об'єкт класу NSNull, який веде себе зовсім інакше і не використовується дуже багато на практиці).


Objective-C зробив річ Null Object / Null Pointer дійсно розмитою. nilотримати #define'd до NULL і поводиться як нульовий об'єкт. Це особливість виконання, коли в C # / Java нульові покажчики видає помилки.
Макшон Чан

0

Не використовуйте жодного, якщо ви можете цього уникнути. Використовуйте заводську функцію, яка повертає тип опції, кортеж або виділений тип суми. Іншими словами, представляйте значення, яке може бути дефолтом, і відрізняється від гарантованого значення, яке не підлягає дефолту.

Спочатку давайте перелічимо desiderata, а потім подумаємо, як ми можемо це виразити кількома мовами (C ++, OCaml, Python)

  1. зловити небажане використання об'єкта за замовчуванням під час компіляції.
  2. зробіть очевидним, чи є задане значення потенційно дефолтним чи ні під час читання коду.
  3. виберіть наше розумне значення за замовчуванням, якщо це застосовно, один раз на тип . Однозначно не вибирайте потенційно різного за замовчуванням на кожному сайті виклику .
  4. полегшують інструменти статичного аналізу або людину, grepяка полює за потенційними помилками.
  5. Для деяких програм програма повинна продовжуватись нормально, якщо несподівано дано значення за замовчуванням. Для інших програм програма повинна негайно зупинитися, якщо їй задано значення за замовчуванням, в ідеалі інформативно.

Я думаю, що напруга між Null Object Pattern і нульовими покажчиками походить від (5). Якщо ми зможемо виявити помилки досить рано, (5) стає суперечливим.

Розглянемо цю мову за мовою:

C ++

На мою думку, клас C ++, як правило, може бути конструктований за замовчуванням, оскільки він полегшує взаємодію з бібліотеками та полегшує використання класу в контейнерах. Це також спрощує успадкування, оскільки вам не потрібно думати про те, до якого конструктора надкласового класу слід звертатися.

Однак це означає, що ви не можете точно знати, чи є тип типу MyClassу "стані за замовчуванням" чи ні. Окрім того, щоб викласти в bool nonemptyполе чи подібне поле до поверхневої настройки за замовчуванням під час виконання, найкраще, що ви можете зробити, це створити нові екземпляри MyClassтаким чином, щоб змусити користувача перевірити це .

Я рекомендую використовувати заводську функцію, яка повертає a std::optional<MyClass>, std::pair<bool, MyClass>або посилання r-значення, std::unique_ptr<MyClass>якщо це можливо.

Якщо ви хочете, щоб ваша заводська функція повертала якийсь стан "заповнювача", який відрізняється від побудованого за замовчуванням MyClass, використовуйте клавішу a std::pairі обов'язково документуйте, що ваша функція це робить.

Якщо заводська функція має унікальну назву, її легко grepназвати і шукати неохайність. Однак це важко grepдля випадків, коли програміст мав використовувати заводську функцію, але цього не зробив .

OCaml

Якщо ви використовуєте таку мову, як OCaml, ви можете просто використовувати optionтип (у стандартній бібліотеці OCaml) або eitherтип (не в стандартній бібліотеці, але легко прокрутити свою власну). Або defaultableтип (я складаю термін defaultable).

type 'a option =
    | None
    | Some of 'a

type ('a, 'b) either =
    | Left of 'a
    | Right of 'b

type 'a defaultable =
    | Default of 'a
    | Real of 'a

Як defaultableпоказано вище, краще, ніж пара, тому що користувач повинен узгодити шаблон для вилучення 'aта не може просто ігнорувати перший елемент пари.

Еквівалент defaultableтипу, як показано вище, може використовуватися в C ++, використовуючи std::variantдва екземпляри одного типу, але частини std::variantAPI непридатні, якщо обидва типи однакові. Крім того, це дивне використання, std::variantоскільки його конструктори типу не називаються.

Пітон

Ви все одно не отримаєте перевірку часу компіляції для Python. Але, динамічно вводячись, зазвичай немає обставин, коли для розміщення компілятора вам потрібен екземпляр-заповнювач місця.

Я рекомендую просто кинути виняток, коли вас змусять створити екземпляр за замовчуванням.

Якщо це не прийнятно, я б рекомендував створити файл, DefaultedMyClassякий успадковується MyClassта повертається з фабричної функції. Це купує вам гнучкість з точки зору "витіснення" функціональності у випадках, що не відповідають вимогам.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.