Якщо null є поганим, чому сучасні мови його реалізують? [зачинено]


82

Я впевнений, що дизайнери таких мов, як Java або C # знали проблеми, пов'язані з наявністю нульових посилань (див. Чи справді нульові посилання погані? ). Також реалізація типу опції насправді не набагато складніша за нульові посилання.

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

Це просто через консерватизм - "в інших мовах це є, у нас його теж є ..."?


99
null великий. Я люблю це і використовую щодня.
Пітер Б

17
@PieterB Але ви використовуєте його для більшості посилань, або ви хочете, щоб більшість посилань не були нульовими? Аргумент не в тому, що не повинно бути зведених даних, а лише те, що вони повинні бути явними та відключеними.

11
@PieterB Але коли більшість не має бути зведеною нанівець, чи не має сенсу робити нульову здатність винятком, а не типовим? Зауважте, що, хоча звичайна конструкція типів опцій полягає в тому, щоб змусити явну перевірку відсутності та розпакування, можна також мати відому семантику Java / C # / ... для вибору змінних посилань (використовуйте, якби не нульовий, підірвіть якщо нуль). Це принаймні запобігло б деякі помилки та зробило б статичний аналіз, який скаржиться на відсутність нульових перевірок набагато практичніше.

20
WTF з вами, хлопці? З усіх речей, які можуть і не можуть помилитися з програмним забезпеченням, намагатися зняти нуль - це зовсім не проблема. ВИНАГИ генерує AV / segfault і так виправляється. Чи існує стільки дефіциту помилок, що вам доведеться переживати з цього приводу? Якщо так, у мене є багато запасних, і жоден з них не викликає проблем із нульовими посиланнями / покажчиками.
Мартін Джеймс

13
@MartinJames "ВИНАГО генерує AV / segfault і так виправляється" - ні, ні, це не так.
detly

Відповіді:


97

Відмова: Оскільки я особисто не знаю жодного дизайнера мови, будь-яка відповідь, яку я даю вам, буде умоглядною.

Від самого Тоні Хоара :

Я називаю це моєю помилкою в мільярд доларів. Це був винахід нульової посилання в 1965 році. Тоді я розробляв першу комплексну систему для посилань на об'єктно-орієнтованій мові (ALGOL W). Моя мета полягала в тому, щоб будь-яке використання посилань було абсолютно безпечним, а перевірка виконувалась компілятором автоматично. Але я не втримався від спокуси ввести нульову посилання просто тому, що це було так просто здійснити. Це призвело до незліченних помилок, уразливості та збоїв у системі, які, ймовірно, спричинили біль і шкоду в мільярд доларів за останні сорок років.

Наголос мій.

Природно, це йому тоді не здавалося поганою ідеєю. Цілком ймовірно, що це частково увійшло з тієї самої причини - якщо це було б непоганою ідеєю винахіднику тюрінського нагородження вигадливого стилю, не дивно, що багато людей досі не розуміють, чому це зло. Це також частково, тому що нові мови зручні бути подібними до старих, як з маркетингових, так і з кривих вивчення. Справа в точці:

"Ми були після програмістів на C ++. Багато з них нам вдалося перетягнути приблизно на півдорозі до Ліспа". -Гуй Стіл, співавтор специфікації Java

(Джерело: http://www.paulgraham.com/icad.html )

І, звичайно, C ++ має null, оскільки C має null, і не потрібно вникати в історичний вплив C. C # витіснив J ++, що було впровадженням Java від Microsoft, і також замінив C ++ як мову вибору для розробки Windows, тому він міг отримати його з будь-якого.

EDIT Ось ще одна цитата Хоара, яку варто врахувати:

Мови програмування в цілому є набагато складнішими, ніж раніше: орієнтація на об'єкти, успадкування та інші особливості все ще не розглядаються з точки зору цілісної та науково обґрунтованої дисципліни чи теорії правильності . Мій оригінальний постулат, котрий я все життя дотримувався вченого, полягає в тому, що кожен використовує критерії коректності як засіб наближення до гідного дизайну мови програмування - той, який не встановлює пасток для своїх користувачів, а також тих, котрий різні компоненти програми чітко відповідають різним компонентам його специфікації, тож ви можете скласти обґрунтовану думку про це. [...] Інструменти, включаючи компілятор, повинні базуватися на певній теорії того, що означає написати правильну програму. - усне інтерв'ю історії Філіпа Л. Франа, 17 липня 2002 р., Кембридж, Англія; Інститут Чарльза Беббіджа, Університет Міннесоти. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

Знову наголос мій. Sun / Oracle та Microsoft - це компанії, а суть будь-якої компанії - це гроші. Користь для них від того, що nullвони могли перевершити мінуси, або у них просто був занадто жорсткий термін, щоб повністю розглянути це питання. Як приклад різного мовного помилок, який, мабуть, стався через строки

Прикро, що Cloneable зламаний, але це трапляється. Оригінальні API API Java були зроблені дуже швидко в стислі терміни, щоб зустріти закриття вікна ринку. Оригінальна команда Java зробила неймовірну роботу, але не всі API є ідеальними. Клоніруемость - слабке місце, і я думаю, що люди повинні усвідомлювати його обмеження. -Джош Блох

(Джерело: http://www.artima.com/intv/bloch13.html )


32
Шановний представник: як я можу покращити свою відповідь?
Довал

6
Ви насправді не відповіли на запитання; ви надали лише деякі цитати про деякі фактичні погляди та деякі додаткові розмахування руками про "вартість". (Якщо нуль є помилкою в мільярд доларів, чи не повинні долари, заощаджені MS та Java, зменшивши цей борг?)
DougM

29
@DougM Що ти очікуєш від мене, натисніть на кожного мовного дизайнера за останні 50 років і запитайте його, чому він реалізував nullйого мовою? Будь-яка відповідь на це питання буде спекулятивною, якщо вона не надходить від мовного конструктора. Я не знаю жодного такого частого сайту, окрім Еріка Ліпперта. Остання частина - це червона оселедець з численних причин. Кількість коду третьої сторони, написаного поверх API та MS та Java, очевидно, перевищує кількість коду в самому API. Тож якщо ваші клієнти хочуть null, ви їх даєте null. Ви також гадаєте, що вони прийняли nullкоштувати їм грошей.
Довал

3
Якщо єдина відповідь, яку ви можете дати, - умоглядна, це чітко зазначайте у вступному параграфі. (Ви запитали, як ви можете покращити свою відповідь, і я відповів. Будь-яка дужка - це лише коментар, який ви можете ігнорувати; саме тому дужки є англійською мовою, зрештою.)
DougM

7
Ця відповідь розумна; Я додав ще кілька міркувань у моєму. Зауважу, що ICloneableаналогічно порушено в .NET; на жаль, це одне місце, де недоліки Java не були вивчені вчасно.
Ерік Ліпперт

121

Я впевнений, що дизайнери таких мов, як Java або C # знали проблеми, пов'язані з наявністю нульових посилань

Звичайно.

Також реалізація типу опції насправді не набагато складніша за нульові посилання.

Дозволю собі не погодитися! Дизайнерські міркування, які перейшли у значення, що змінюються на значеннях у C # 2, були складними, суперечливими та складними. Вони взяли дизайнерські команди як мов, так і тривалості багатомісячних дискусій, впровадження прототипів тощо, і фактично семантика незмінного боксу була змінена дуже близько до доставки C # 2.0, що було дуже суперечливим.

Чому вони вирішили все-таки включити його?

Весь дизайн - це процес вибору серед безлічі тонко і грубо несумісних цілей; Я можу дати лише короткий ескіз лише кількох факторів, які були б враховані:

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

  • Ознайомлення з існуючими користувачами C, C ++ та Java є важливим.

  • Важлива легка сумісність із COM.

  • Важлива легка сумісність з усіма іншими мовами .NET.

  • Важлива легка взаємодія з базами даних.

  • Послідовність семантики важлива; якщо у нас є посилання на TheKingOfFrance, що дорівнює нулю, це завжди означає "немає короля Франції прямо зараз", або це може означати також "Однозначно є король Франції; я просто не знаю, хто це зараз"? чи це може означати "саме поняття мати короля у Франції є безглуздим, тому навіть не задайте питання!"? Нуль може означати всі ці речі та інше в C #, і всі ці поняття є корисними.

  • Вартість продуктивності важлива.

  • Важливим є прихильність до статичного аналізу.

  • Послідовність системи типів важлива; чи можемо ми завжди знати, що нерегульована посилання ніколи за жодних обставин не виявляється недійсною? А як щодо конструктора об'єкта з ненульовим полем опорного типу? А як щодо фіналізатора такого об’єкта, де об’єкт доопрацьовується, оскільки код, який повинен був заповнити посилання, кинув виняток ? Система типу, яка лежить вам щодо своїх гарантій, небезпечна.

  • А як щодо послідовності семантики? Нульові значення поширюються при використанні, але нульові посилання кидають винятки, коли вони використовуються. Це непослідовно; це невідповідність виправдана якоюсь вигодою?

  • Чи можемо ми реалізувати функцію, не порушуючи інших функцій? Які ще можливі майбутні функції виключає ця функція?

  • Ви йдете на війну з вашою армією, а не з тією, яку вам хотілося б. Пам'ятайте, що у C # 1.0 не було дженериків, тому говорити про Maybe<T>альтернативу - це повний нестандарт. Чи повинно .NET прослідковуватись протягом двох років, поки команда виконання додала загальну інформацію, виключно для усунення нульових посилань?

  • А як щодо узгодженості системи типів? Ви можете сказати Nullable<T>для будь-якого типу значення - ні, чекайте, це брехня. Ви не можете сказати Nullable<Nullable<T>>. Чи зможете ви? Якщо так, то яка його бажана семантика? Чи варто змусити всю систему типів мати спеціальний корпус саме для цієї функції?

І так далі. Ці рішення є складними.


12
+1 для всього, але особливо для підбору дженериків. Неважко забути, що в історії Java та C # були періоди часу, де генерики не існували.
Довал

2
Може бути тупим запитанням (я просто студент з інформаційних технологій) - але не вдалося реалізувати тип варіанту на рівні синтаксису (CLR нічого про це не знає) як звичайна зведена посилання, яка вимагає перевірки "значення" перед використанням у код? Я вважаю, що варіанти варіантів не потребують перевірок під час виконання.
mrpyo

2
@mrpyo: Звичайно, це можливий вибір впровадження. Жоден з інших варіантів дизайну не зникає, і цей вибір реалізації має багато плюсів і мінусів.
Ерік Ліпперт

1
@mrpyo Я думаю, що змушувати перевірку "має значення" - це не дуже гарна ідея. Теоретично це дуже гарна ідея, але на практиці IMO він приноситиме всілякі порожні чеки, просто щоб задовольнити компілятор - як перевірені винятки на Java, і люди, обдуривши це catches, нічого не роблять. Я думаю, що краще дозволити системі підірватись, а не продовжувати роботу в можливо недійсному стані.
Нічого не можна,

2
@voo: Масиви незв’язного довідкового типу важкі з багатьох причин. Можливих рішень, і всі вони накладають витрати на різні операції. Пропозиція Supercat полягає в тому, щоб відстежити, чи можна законно прочитати елемент перед його призначенням, що покладає витрати. Ваше - переконатися, що ініціалізатор працює на кожному елементі до того, як масив стане видимим, що накладає інший набір витрат. Отож, ось що: незважаючи на те, яку з цих методик вибирати, хтось збирається поскаржитися, що це неефективно для їх домашнього вихованця. Це серйозні моменти проти функції.
Ерік Ліпперт

28

Null служить дуже поважною метою представляти відсутність цінності.

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

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

Приклад, що виправдовує нулі:

Дата смерті, як правило, є нульовим полем. Можливі три ситуації з датою смерті. Або людина померла, і дата відома, людина померла, і дата невідома, або людина не померла, а тому дата смерті не існує.

Дата смерті також є полем DateTime і не має значення "невідомо" або "порожнє". У нього є дата за замовчуванням, яка з’являється, коли ви вносите новий час, який змінюється залежно від використовуваної мови, але технічно є ймовірність, що людина насправді загинув у той час і позначив би ваше "порожнє значення", якби ви використовувати дату за замовчуванням.

Дані повинні бути належним чином відображати ситуацію.

Людина померла дата смерті відома (9.3.1984)

Простий, "9.09.1984"

Людина померла дата смерті невідома

То що найкраще? Нульовий , "0/0/0000" або "01 / 01/1869 "(або все, що вказано за замовчуванням?)

Особа не померла, дата смерті не застосовується

То що найкраще? Нульовий , "0/0/0000" або "01 / 01/1869 "(або все, що вказано за замовчуванням?)

Тож давайте подумаємо про кожне значення над ...

  • Нуль , це має наслідки та проблеми, з якими потрібно бути обережними, випадково намагаючись маніпулювати нею, не підтверджуючи, що спочатку це не нульово, наприклад, викине виняток, але це також найкраще відображає реальну ситуацію ... Якщо людина не загинула дата смерті не існує ... це нічого ... це недійсне ...
  • 0/0/0000 , Це може бути добре в деяких мовах і навіть може бути відповідним поданням про відсутність дати. На жаль, деякі мови та валідація відкинуть це як недійсний час, що у багатьох випадках робить його недійсним.
  • 1/1/1869 (або будь-яке значення для дати за замовчуванням) , проблема тут полягає в тому, що це стає складним в обробці. Ви можете використати це як свою відсутність значущості, за винятком того, що трапиться, якщо я хочу відфільтрувати всі свої записи, у яких я не маю дати смерті? Я міг легко відфільтрувати людей, які насправді померли в цю дату, що може спричинити проблеми з цілісністю даних.

Той факт , іноді ви чи потрібно не представляти нічого і впевнений , що іноді змінна типу добре працює, але часто типи змінних повинні бути в змозі уявити нічого.

Якщо у мене немає яблук, у мене є 0 яблук, але що робити, якщо я не знаю, скільки яблук у мене?

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


37
Null serves a very valid purpose of representing a lack of value.OptionАбо Maybeтипу служить цей дуже вагома причина , НЕ в обхід системи типів.
Довал

34
Ніхто не сперечається з тим, що не повинно бути недостатніх значень, вони стверджують, що значення, які можуть бути відсутні, повинні бути явно позначені як такі, а не кожне значення, яке потенційно відсутнє.

2
Я думаю, що RualStorge говорив по відношенню до SQL, тому що є табори, в яких кожен стовпець повинен бути позначений як NOT NULL. Моє запитання не стосувалося RDBMS, хоча ...
mrpyo

5
+1 для розрізнення "без значення" та "невідомого значення"
Девід

2
Чи не було б більше сенсу відокремлювати стан людини? Тобто Personтип має stateполе типу State, яке є дискримінаційним об'єднанням Aliveі Dead(dateOfDeath : Date).
jon-hanson

10

Я б не пішов так далеко, як "інші мови мають, ми також повинні його мати" ... як би це було якось іти в ногу з Джонісом. Ключовою особливістю будь-якої нової мови є можливість взаємодії з існуючими бібліотеками іншими мовами (читати: C). Оскільки C має нульові покажчики, то рівень інтероперабельності обов'язково потребує поняття null (або якийсь інший еквівалент "не існує", який вибухає при його використанні).

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

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


3
Перший абзац для мене не має сенсу. У Java немає C interop у запропонованій вами формі (є JNI, але вона вже стрибає через десяток обручів для всього, що стосується посилань; плюс це рідко використовується на практиці), як і для інших "сучасних" мов.

@delnan - вибачте, я більше знайомий з C #, який має такий інтероп. Я швидше припускав, що багато фундаментальних бібліотек Java також використовують JNI внизу.
Теластин

6
Ви робите хороший аргумент щодо дозволу null, але ви можете дозволити null, не заохочуючи його. Скала - хороший приклад цього. Він може безперешкодно взаємодіяти з Java apis, які використовують null, але вам рекомендується зафіксувати його Optionдля використання в Scala, що так само просто val x = Option(possiblyNullReference). На практиці люди не потребують багато часу, щоб побачити переваги Option.
Карл Білефельдт

1
Типи варіантів йдуть поруч із (статично підтвердженим) узгодженням шаблону, якого C #, на жаль, не має. Хоча F # робить, і це чудово.
Стівен Еверс

1
@SteveEvers Можна підробити це, використовуючи абстрактний базовий клас із приватним конструктором, запечатаними внутрішніми класами та Matchметодом, який бере аргументи делегатів. Потім ви передаєте лямбда-вирази до Match(бонусні бали за використання названих аргументів) і Matchвикликаєте правильний.
Довал

7

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

Дозволення nullпосилань (і покажчиків) - лише одна реалізація цієї концепції, і, можливо, найпопулярніша, хоча, як відомо, є проблеми: C, Java, Python, Ruby, PHP, JavaScript, ... всі використовують подібне null.

Чому? Ну, яка альтернатива?

У функціональних мовах, таких як Haskell, ви маєте Optionабо Maybeтип; однак ці основані на:

  • параметричні типи
  • алгебраїчні типи даних

Тепер, чи підтримували оригінали C, Java, Python, Ruby або PHP будь-яку з цих функцій? Ні. Недосконалі дженерики Java є останніми в історії мови, і я якось сумніваюся, що інші їх взагалі реалізують.

Там у вас є. nullлегко, параметричні алгебраїчні типи даних важче. Люди пішли на найпростішу альтернативу.


+1 для "null - легко, параметричні алгебраїчні типи даних важче". Але я думаю, що це було не стільки проблемою параметричного набору тексту, скільки ADT, а важче; це просто те, що вони не сприймаються як необхідні. Якби Java постачалася без об'єктної системи, з іншого боку, вона б провалилася; OOP була "функцією виставок", оскільки, якщо у вас її не було, ніхто не зацікавився.
Doval

@Doval: ну OOP, можливо, був би необхідний для Java, але це не для C :) Але це правда, що Java спрямована на те, щоб бути простою. На жаль, люди, здається, припускають, що проста мова призводить до простих програм, що дивно (Brainfuck - це дуже проста мова ...), але ми, звичайно, погоджуємось, що складні мови (C ++ ...) не є панацеєю, навіть якщо вони можуть бути неймовірно корисними.
Матьє М.

1
@MatthieuM. Реальні системи є складними. Добре розроблена мова, складність якої відповідає реальній системі, що моделюється, може дозволити моделювати складну систему простим кодом. Спроби спростити мову просто підштовхують складність до програміста, який ним користується.
supercat

@supercat: Я більше не міг погодитися. Або як Ефштейн перефразовується: "Зробіть все максимально просто, але не простіше".
Матьє М.

@MatthieuM. Ейнштейн був мудрий у багатьох аспектах. Мови, які намагаються припустити, що "все є об'єктом, посилання на який може бути збережено Object", не визнають, що практичні програми потребують нерозподілених змінних об'єктів і змінних незмінних об'єктів (обидва вони повинні поводитись як значення), а також роздільні та неподільні сутностей. Використання одного Objectтипу для всього не виключає потреби в таких відмінностях; це просто ускладнює їх правильне використання.
supercat

5

Нуль / нуль / ніхто сам по собі не є злим.

Якщо ви спостерігаєте за його оманливо названим відомим виступом "Помилка мільярда доларів", Тоні Хоар розповідає про те, як дозволити будь-якій змінній змогти визначити нуль було величезною помилкою. Альтернатива - використання параметрів - насправді не позбавляється від нульових посилань. Натомість це дозволяє вказати, які змінні можуть містити нульові, а які ні.

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

Тож виникає питання, чому мови не реалізують Options? Власне кажучи, імовірно найпопулярніша мова всіх часів C ++ має можливість визначати змінні об'єкта, які неможливо призначити NULL. Це рішення "нульової проблеми", про який Тоні Хоаре згадував у своєму виступі. Чому наступна найпопулярніша набрана мова, Java, не має її? Можна запитати, чому у нього загалом стільки вад , особливо в її типній системі. Я не думаю, що ви справді можете сказати, що мови систематично роблять цю помилку. Деякі так, деякі ні.


1
Однією з найбільших сильних сторін Java з точки зору впровадження, але слабкими з точки зору мови є те, що існує лише один непомітивний тип: Promiscuous Object Reference. Це надзвичайно спрощує час виконання, завдяки чому можливі надзвичайно легкі реалізації JVM. Цей дизайн, однак, означає, що для кожного типу повинно бути значення за замовчуванням, а для Promiscuous Object Reference єдиним можливим типовим є null.
supercat

Що ж, один кореневий непомітивний тип у будь-якому випадку. Чому це слабкість з мовної точки зору? Я не розумію, чому цей факт вимагає, щоб кожен тип мав значення за замовчуванням (або, навпаки, чому кілька типів коренів дозволяють типам не мати значення за замовчуванням), а також чому це слабкість.
BT

Які ще види непомітивів можуть містити поле чи елемент масиву? Слабка слабкість полягає в тому, що деякі посилання використовуються для інкапсуляції ідентичності, а деякі для інкапсуляції значень, що містяться в ідентифікованих таким чином об'єктах. Для змінних типів опорного типу, які використовуються для інкапсуляції ідентичності, nullє єдиним розумним замовчуванням. Посилання, які використовуються для інкапсуляції значення, однак, можуть мати розумну поведінку за замовчуванням у випадках, коли тип мав би або міг побудувати розумний екземпляр за замовчуванням. Багато аспектів того, як повинні поводитись посилання, залежать від того, і як вони інкапсулюють значення, але ...
supercat

... система типу Java не може цього виразити. Якщо fooєдине посилання на int[]вміст {1,2,3}і код хоче fooмати посилання на int[]вміст {2,2,3}, найшвидшим способом досягти цього було б збільшення foo[0]. Якщо код хоче повідомити методу, що fooмає місце {1,2,3}, інший метод не змінить масив і не збереже посилання поза точкою, де він fooхотів би його змінити, найшвидшим способом досягти цього було б передати посилання на масив. Якщо у Java був тип "ефемерної довідки лише для читання", то ...
supercat

... масив міг би бути переданий безпечно як ефемерне посилання, і метод, який хотів зберегти своє значення, знав би, що його потрібно скопіювати. За відсутності такого типу, єдиним способом безпечного викриття вмісту масиву є або зробити його копію, або інкапсулювати в об'єкт, створений саме для цієї мети.
supercat

4

Оскільки мови програмування, як правило, розроблені як практично корисні, а не технічно правильні. Справа в тому, що nullстани - це звичайне явище через погані або відсутні дані або стан, який ще не вирішений. Технічно найкращі рішення є більш громіздкими, ніж просто дозволяти нульовим станам і висмоктувати факт, що програмісти роблять помилки.

Наприклад, якщо я хочу написати простий скрипт, який працює з файлом, я можу написати псевдокод на зразок:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

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


13
І ось ви навели приклад того, що саме не так з нулями. Правильно реалізована функція "openfile" повинна викинути виняток (для відсутнього файлу), який зупинить виконання саме там із точним поясненням того, що сталося. Замість цього, якщо він повертає null, він поширюється далі (to for line in file) і викидає безглузді виключення з нульовим посиланням, що нормально для такої простої програми, але викликає реальні проблеми налагодження в набагато складніших системах. Якщо нулів не існувало, дизайнер "openfile" не зміг би зробити цю помилку.
mrpyo

2
+1 за "Оскільки мови програмування, як правило, розроблені як практично корисніші, а не технічно правильні"
Мартін Бай

2
Кожен тип опцій, про який я знаю, дозволяє вам робити відмову від нуля за допомогою одного короткого додаткового виклику методу (приклад Rust:) let file = something(...).unwrap(). Залежно від вашої POV, це простий спосіб не обробляти помилки чи стислий твердження про те, що нуль не може виникнути. Витрачений час мінімальний, і ви економите час в інших місцях, оскільки вам не доведеться розбиратися, чи може щось бути недійсним. Ще одна перевага (яка сама по собі може бути вартим додаткового дзвінка) полягає в тому, що ви явно ігноруєте випадок помилок; коли це не вдається, мало сумнівів, що пішло не так і куди потрібно виправити помилку.

4
@mrpyo Не всі мови підтримують винятки та / або обробку винятків (a la try / catch). І винятки можна також зловживати - "виняток як контроль потоку" є поширеною антидіаграмою. Цей сценарій - файл не існує - є AFAIK - найбільш часто цитованим прикладом цього антидіаграму. Здається, ви замінюєте одну погану практику іншою.
Девід

8
@mrpyo if file exists { open file }страждає від перегонів. Єдиний надійний спосіб дізнатися, чи вдало відкрити файл - спробувати відкрити його.

4

Існує чітке, практичне використання покажчика NULL(або nil, або Nil, або null, Nothingабо, як воно називається, у вибраній вами мові).

Для тих мов, які не мають системи винятків (наприклад, C), нульовий вказівник може бути використаний як позначка помилки при поверненні вказівника. Наприклад:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Тут NULLповернений з malloc(3)використовується як маркер відмови.

Якщо використовується в аргументах методу / функції, він може вказувати на використання аргументу за замовчуванням або ігнорувати вихідний аргумент. Приклад нижче.

Навіть для тих мов, що мають механізм винятку, нульовий покажчик може використовуватися як індикатор м'якої помилки (тобто помилок, які підлягають відшкодуванню), особливо коли обробка виключень є дорогою (наприклад, Objective-C):

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

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


5
Проблема полягає в тому, що немає способу відрізнити змінні, які ніколи не повинні містити, nullвід тих, що повинні. Наприклад, якщо я хочу нового типу, який містить 5 значень у Java, я міг би використовувати enum, але те, що я отримую, - це тип, який може містити 6 значень (5 я хотів + null). Це вада в системі типів.
Довал

@Doval Якщо це ситуація, просто призначте NULL значення (або якщо у вас за замовчуванням, розглядайте його як синонім значення за замовчуванням) або використовуйте NULL (який ніколи не повинен з’являтися в першу чергу) як маркер м'якої помилки (тобто помилка, але принаймні ще не виходить з ладу)
Maxthon Chan

1
@MaxtonChan Nullможе присвоювати значення лише тоді, коли значення типу не несуть даних (наприклад, значення enum). Як тільки ваші значення стають щось складніше (наприклад, структура), nullне можна присвоювати значення, яке має сенс для цього типу. Немає можливості використовувати nullструктуру чи список. І, знову ж таки, проблема використання nullсигналу про помилку полягає в тому, що ми не можемо сказати, що може повернути нуль або прийняти нуль. Будь-яка змінна у вашій програмі може бути, nullякщо ви не будете надзвичайно прискіпливо перевіряти кожну, nullперед кожним використанням, чого ніхто не робить.
Довал

1
@Doval: Не було б особливих труднощів у тому, щоб незмінний тип опорного типу розглядався nullяк корисне значення за замовчуванням (наприклад, значення за замовчуванням stringповодиться як порожній рядок, як це було зроблено у попередній загальній моделі об'єкта). Все, що було б потрібно, було б для використання мов, callа не callvirtдля виклику невіртуальних членів.
supercat

@supercat Це хороший момент, але тепер вам не потрібно додавати підтримку для розмежування між непорушними та непорушними типами? Я не впевнений, наскільки тривіально це додати до мови.
Довал

4

Є два пов'язані, але трохи різні питання:

  1. Чи повинні nullвзагалі існувати? Або завжди слід використовувати Maybe<T>там, де null корисний?
  2. Чи повинні всі посилання бути нульовими? Якщо ні, що має бути типовим?

    Наявність явного оголошення нульових посилальних типів string?подібними або подібними дозволить уникнути більшості (але не всіх) nullпричин проблем , не надто відрізняючись від тих, до яких звикли програмісти.

Я принаймні погоджуюся з вами, що не всі посилання повинні бути зведені нанівець. Але уникнення нуля не обходиться без його складностей:

.NET ініціалізує всі поля до того, default<T>як до них вперше можна отримати доступ керованим кодом. Це означає, що для типів посилань вам потрібен nullабо щось еквівалентне, і типи значень можна ініціалізувати до якогось нуля без запущеного коду. Незважаючи на те, що вони мають серйозні недоліки, простота defaultініціалізації, можливо, переважала ці недоліки.

  • Для полів екземпляра ви можете обійти цю проблему , вимагаючи ініціалізацію полів перед тим оголюючи thisпокажчик на керований код. Spec # пройшов цей маршрут, використовуючи інший синтаксис від ланцюга конструктора порівняно з C #.

  • Для статичних полів забезпечити це складніше, якщо ви не ставите сильних обмежень щодо того, який код може працювати в ініціалізаторі полів, оскільки ви не можете просто приховати thisпокажчик.

  • Як ініціалізувати масиви референтних типів? Розглянемо, List<T>який підтримується масивом, місткістю більшою за довжину. Решта елементів повинні мати певне значення.

Інша проблема полягає в тому, що він не дозволяє такі методи, як bool TryGetValue<T>(key, out T value)повернення default(T), valueніби вони нічого не знаходять. Хоча в цьому випадку легко стверджувати, що вихідний параметр - це поганий дизайн, в першу чергу, і цей метод повинен повернути дискримінаційний союз або, можливо, замість цього.

Усі ці проблеми можна вирішити, але це не так просто, як "заборонити нуль і все добре".


List<T>ІМХО найкращий приклад, тому що це зажадає або , що кожне Tмає значення за замовчуванням, що кожен елемент в резервному сховище бути Maybe<T>з додатковим полем «IsValid», навіть якщо Tце Maybe<U>, або що код List<T>поводяться по- різному в залежності на те, чи Tє сам по собі нульовим типом. Я б вважав ініціалізацію T[]елементів до значення за замовчуванням найменшим злом із цих варіантів, але це, звичайно, означає, що для елементів потрібно мати значення за замовчуванням.
supercat

Іржа слідує за пунктом 1 - нуль взагалі немає. Цейлон відповідає пункту 2 - за замовчуванням недійсний. Посилання, які можуть бути нульовими, явно оголошуються типом об'єднання, що включає в себе або посилання, або нуль, але null ніколи не може бути значенням простої посилання. Як результат, мова є абсолютно безпечною і немає NullPointerException, оскільки це семантично неможливо.
Джим Балтер

2

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

Різні мови та системи застосовують різні підходи.

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

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

  • Третій підхід - просто ігнорувати проблему і нехай все, що сталося, «природно» просто відбудеться.

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

Підхід №4 набагато безпечніший, ніж підхід №3, і загалом дешевший, ніж підходи №1 та №2. Тоді залишається питання про те, яким має бути значення за замовчуванням для еталонного типу. Для незмінних типів посилань у багатьох випадках має сенс визначити екземпляр за замовчуванням і сказати, що за замовчуванням для будь-якої змінної цього типу має бути посилання на цей екземпляр. Для змінних посилальних типів, однак, це не буде дуже корисно. Якщо буде зроблена спроба використовувати змінний тип посилання до того, як вона була написана, зазвичай не існує жодного безпечного курсу дій, окрім пастки в точці спроби використання.

Семантичний кажучи, якщо один має масив customersтипу Customer[20]і один спробу , Customer[4].GiveMoney(23)НЕ супроводжуються збережене нічого Customer[4], виконання матиме пастку. Можна стверджувати, що спроба читання Customer[4]повинна негайно GiveMoneyпотрапляти в пастку, а не чекати, поки не буде спроби коду , але є достатньо випадків, коли корисно прочитати слот, з’ясувати, що він не містить значення, а потім скористатися цим інформація про те, що спроба читання сама по собі виявляється невдалою, часто є великою неприємністю.

Деякі мови дозволяють вказати, що певні змінні ніколи не повинні містити null, а будь-яка спроба збереження нуля повинна викликати негайну пастку. Це корисна особливість. Однак загалом, будь-яка мова, яка дозволяє програмістам створювати масиви посилань, повинна або передбачати можливість нульових елементів масиву, або змусити ініціалізацію елементів масиву до даних, які, можливо, не мають значення.


Чи не буде Maybe/ Optionтип вирішити проблему з # 2, так як якщо ви не маєте значення для вашої довідки поки , але буде мати один в майбутньому, ви можете просто зберігати Nothingв Maybe <Ref type>?
Довал

@Doval: Ні, це не вирішило б проблему - принаймні, не без введення нульових посилань заново. Чи повинен "нічого" діяти як член типу? Якщо так, то який? Або він повинен кинути виняток? У такому разі як вам краще, ніж просто nullправильно / розумно використовувати?
cHao

@Doval: Чи слід підтримувати тип підкладки List<T>a T[]чи a Maybe<T>? Що з типом підкладки List<Maybe<T>>?
supercat

@supercat Я не впевнений, яким чином Maybeмає сенс типу резервного копіювання, Listоскільки він Maybeмістить єдине значення. Ви мали на увазі Maybe<T>[]?
Довал

@cHao Nothingможна призначати лише значенням типу Maybe, тому це не зовсім схоже на призначення null. Maybe<T>і Tє два різних типи.
Довал
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.