Если ноль плох, почему современные языки реализуют его? [закрыто]

82

Я уверен, что разработчики языков, таких как Java или C #, знали проблемы, связанные с существованием нулевых ссылок (см. Действительно ли нулевые ссылки - это плохо? ). Также реализация типа параметра не намного сложнее, чем нулевые ссылки.

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

Это просто из-за консерватизма - «у других языков есть, у нас тоже должно быть ...»?

mrpyo
источник
99
ноль это здорово. Я люблю это и использую это каждый день.
Питер Б,
17
@PieterB Но вы используете его для большинства ссылок, или вы хотите, чтобы большинство ссылок не было нулевым? Аргумент не в том, что данные не должны быть обнуляемыми, а в том, что они должны быть явными и подписанными.
11
@PieterB Но когда большинство не должно быть обнуляемым, имеет ли смысл делать нулевую способность исключением, а не значением по умолчанию? Обратите внимание, что, хотя обычная конструкция типов опций заключается в принудительной явной проверке отсутствия и распаковки, можно также иметь хорошо известную семантику Java / C # / ... для необязательных ссылок opt-in (используйте, как если бы не nullable, взорвать если ноль). Это, по крайней мере, предотвратило бы некоторые ошибки и сделало бы более практичным статический анализ, который бы жаловался на пропущенные нулевые проверки.
20
WTF с вами, ребята? Из всех вещей, которые могут и не работают с программным обеспечением, попытка разыменования нулевого значения не является проблемой. Он ВСЕГДА генерирует AV / segfault и поэтому исправляется. Есть ли такая большая нехватка ошибок, что вы должны беспокоиться об этом? Если так, у меня есть много запасных, и ни один из них не вызывает проблем с нулевыми ссылками / указателями.
Мартин Джеймс
13
@MartinJames «ВСЕГДА генерирует AV / segfault и поэтому исправляется» - нет, нет, это не так.
детально

Ответы:

97

Отказ от ответственности: Поскольку я лично не знаю дизайнеров языка, любой ответ, который я дам вам, будет умозрительным.

От самого Тони Хоара :

Я называю это моей ошибкой в ​​миллиард долларов. Это было изобретение нулевой ссылки в 1965 году. В то время я разрабатывал первую всеобъемлющую систему типов для ссылок на объектно-ориентированном языке (ALGOL W). Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с проверкой, выполняемой автоматически компилятором. Но я не мог удержаться от соблазна вставить нулевую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и сбоям системы, которые, вероятно, причинили миллиард долларов боли и ущерба за последние сорок лет.

Акцент мой.

Естественно, в то время это не казалось ему плохой идеей. Вероятно, что это было увековечено частично по той же причине - если это показалось хорошей идеей изобретателю удостоенной награды Тьюринга быстрой сортировки, неудивительно, что многие люди до сих пор не понимают, почему это зло. Это также, вероятно, отчасти потому, что новым языкам удобно быть похожими на более старые, как по маркетингу, так и по причине кривой обучения. Дело в точке:

«Мы были после программистов на C ++. Нам удалось перетащить их на полпути к Лиспу». -Гай Стил, соавтор спецификации Java

(Источник: http://www.paulgraham.com/icad.html )

И, конечно, C ++ имеет значение null, потому что C имеет значение null, и нет необходимости вдаваться в историческое влияние C. Тип C # заменил J ++, который был реализацией Microsoft Java, и также заменил C ++ как язык выбора для разработки Windows, так что он мог получить его от любого из них.

РЕДАКТИРОВАТЬ Вот еще одна цитата из Hoare стоит рассмотреть:

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

Опять акцент мой. Sun / Oracle и Microsoft - это компании, и суть любой компании - деньги. Преимущества их наличия nullмогли перевесить минусы или просто слишком сжатые сроки, чтобы полностью рассмотреть вопрос. В качестве примера другой языковой ошибки, которая, вероятно, произошла из-за сроков:

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

(Источник: http://www.artima.com/intv/bloch13.html )

Doval
источник
32
Уважаемый downvoter: как я могу улучшить свой ответ?
Довал
6
Вы на самом деле не ответили на вопрос; Вы только предоставили некоторые цитаты о некоторых постфактумных мнениях и некоторые дополнительные ручные помахивания о «стоимости». (Если ноль является ошибкой в ​​миллиард долларов, не должны ли сэкономленные доллары MS и Java путем его реализации уменьшить этот долг?)
DougM
29
@DougM Что вы ожидаете от меня, подбросьте каждого языкового дизайнера за последние 50 лет и спросите его, почему он реализовал nullна своем языке? Любой ответ на этот вопрос будет умозрительным, если он не придет от дизайнера языка. Я не знаю ни одного такого частого сайта, кроме Эрика Липперта. Последняя часть - красная сельдь по многим причинам. Количество стороннего кода, написанного поверх API MS и Java, явно перевешивает количество кода в самом API. Так что, если ваши клиенты хотят null, вы даете им null. Вы также предполагаете, что они приняли null, стоило им денег.
Довал
3
Если единственный ответ, который вы можете дать, является умозрительным, четко укажите это во вступительном абзаце. (Вы спросили, как вы можете улучшить свой ответ, и я ответил. Любая скобка - это просто комментарий, который вы можете игнорировать; в конце концов, для английских скобок это то, что нужно.)
DougM
7
Этот ответ разумен; Я добавил еще несколько соображений в мой. Я отмечаю, что ICloneableаналогичным образом нарушается в .NET; к сожалению, это единственное место, где недостатки Java не были извлечены вовремя.
Эрик Липперт
121

Я уверен, что разработчики таких языков, как Java или C #, знали проблемы, связанные с существованием нулевых ссылок

Конечно.

Также реализация типа параметра не намного сложнее, чем нулевые ссылки.

Позволю себе не согласиться! Вопросы проектирования, которые вошли в типы значений Nullable в C # 2, были сложными, противоречивыми и сложными. Они взяли проектные команды обоих языков и во время выполнения многих месяцев дискуссий, реализации прототипов, и так далее, а на самом деле семантика обнуляемого бокса были изменены очень очень близко к перевозке груза C # 2.0, который был очень спорным.

Почему они решили включить его в любом случае?

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

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

  • Знакомство с существующими пользователями C, C ++ и Java очень важно.

  • Простота взаимодействия с COM важна.

  • Важное значение имеет простота взаимодействия со всеми другими языками .NET.

  • Важное значение имеет простота взаимодействия с базами данных.

  • Согласованность семантики важна; если у нас ссылка TheKingOfFrance равна нулю, значит ли это всегда «сейчас нет короля Франции», или это может также означать «король Франции определенно есть; я просто не знаю, кто он сейчас»? или это может означать «само понятие наличия короля во Франции бессмысленно, так что даже не задавайте вопрос!»? Нуль может означать все эти вещи и многое другое в C #, и все эти понятия полезны.

  • Стоимость исполнения важна.

  • Быть поддающимся статическому анализу важно.

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

  • А как насчет согласованности семантики? Нулевые значения распространяются при использовании, но нулевые ссылки выдают исключения при использовании. Это противоречиво; это несоответствие оправдано какой-то выгодой?

  • Можем ли мы реализовать эту функцию, не нарушая другие функции? Какие другие возможные будущие функции исключает эта функция?

  • Вы идете на войну с армией, которая у вас есть, а не с той, которая вам нужна. Помните, что в C # 1.0 не было дженериков, поэтому говорить об Maybe<T>альтернативе - совсем не стартер. Должно ли .NET проскочить в течение двух лет, пока команда времени выполнения добавила дженерики, исключительно для устранения нулевых ссылок?

  • Как насчет согласованности системы типов? Вы можете сказать Nullable<T>для любого типа значения - нет, подождите, это ложь. Ты не можешь сказать Nullable<Nullable<T>>. Должны ли вы быть в состоянии? Если так, какова его желательная семантика? Стоит ли делать так, чтобы вся система типов имела специальный случай именно для этой функции?

И так далее. Эти решения сложны.

Эрик Липперт
источник
12
+1 за все, но особенно за воспитание дженериков. Легко забыть, что в истории Java и C # были периоды времени, когда генерики не существовали.
Довал
2
Возможно, глупый вопрос (я всего лишь ИТ-студент), но не может быть реализован тип параметра на уровне синтаксиса (с CLR, ничего не знающего об этом) в качестве обычной ссылки на Nullable, которая требует проверки «has-value» перед использованием в код? Я считаю, что типы опций не требуют каких-либо проверок во время выполнения.
mrpyo
2
@mrpyo: Конечно, это возможный вариант реализации. Ни один из других вариантов дизайна не исчезнет, ​​и у этого варианта реализации есть много плюсов и минусов.
Эрик Липперт,
1
@mrpyo Я думаю, что форсировать проверку «имеет значение» не очень хорошая идея. Теоретически это очень хорошая идея, но на практике IMO принесет всевозможные пустые проверки, просто чтобы удовлетворить проверенные компилятором исключения в Java, и люди, обманывающие это catches, ничего не делают. Я думаю, что лучше позволить системе взорваться, чем продолжать работу в возможно неправильном состоянии.
NothingsImpossible
2
@voo: Массивы ненулевого ссылочного типа сложны по многим причинам. Существует множество возможных решений, и все они накладывают затраты на различные операции. Supercat предлагает отслеживать возможность легального чтения элемента до его назначения, что влечет за собой затраты. Ваша задача - убедиться, что инициализатор работает на каждом элементе до того, как массив станет видимым, что влечет за собой другой набор затрат. Итак, вот в чем проблема: независимо от того, какой из этих методов вы выберете, кто-то будет жаловаться, что это неэффективно для сценария с его питомцами. Это серьезные моменты против фича.
Эрик Липперт
28

Нуль служит очень ценной цели представления недостатка стоимости.

Я скажу, что я - самый громкий человек, которого я знаю о злоупотреблениях null и обо всех головных болях и страданиях, которые они могут причинить, особенно когда используются свободно.

Моя личная позиция заключается в том, что люди могут использовать нули только тогда, когда они могут обосновать это необходимостью и уместностью.

Пример, оправдывающий нули:

Дата смерти, как правило, пустое поле. Есть три возможных ситуации с датой смерти. Либо человек умер, и дата известна, человек умер, и дата неизвестна, либо человек не умер, и поэтому дата смерти не существует.

Дата смерти также является полем DateTime и не имеет «неизвестного» или «пустого» значения. У него есть дата по умолчанию, которая появляется, когда вы создаете новую дату и время, которая меняется в зависимости от используемого языка, но технически существует вероятность того, что человек действительно умер в то время и будет помечен как «пустое значение», если вы используйте дату по умолчанию.

Данные должны правильно представлять ситуацию.

Человек мертв Дата смерти известна (09.03.1984)

Простой, 3/9/1984

Человек мертв Дата смерти неизвестна

Так что лучше? Null , '0/0/0000' или '01 / 01/1869 '(или каково ваше значение по умолчанию?)

Человек не умер, дата смерти не применяется

Так что лучше? Null , '0/0/0000' или '01 / 01/1869 '(или каково ваше значение по умолчанию?)

Итак, давайте подумаем над каждым значением ...

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

Тот факт , иногда вы ли нужно не представлять ничего и уверен , что иногда переменная типа хорошо работает, но часто типы переменных должны быть в состоянии представить ничего.

Если у меня нет яблок, у меня 0 яблок, но что, если я не знаю, сколько у меня яблок?

Безусловно, null злоупотребляет и потенциально опасен, но иногда это необходимо. Во многих случаях это только значение по умолчанию, потому что пока я не предоставлю значение, ему не хватает значения, и что-то должно его представлять. (Значение NULL)

RualStorge
источник
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).
Джон Хансон
10

Я бы не пошел так далеко, как «у других языков есть, у нас тоже должно быть…», как будто это некое подобие Джонсам. Ключевой особенностью любого нового языка является возможность взаимодействия с существующими библиотеками на других языках (читай: C). Поскольку C имеет нулевые указатели, для уровня взаимодействия обязательно требуется понятие нулевого (или некоторого другого «не существует» эквивалента, который взрывается при его использовании).

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

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

Telastyn
источник
3
Первый абзац не имеет смысла для меня. Java не имеет взаимодействия с C в той форме, которую вы предлагаете (есть JNI, но она уже перепрыгивает через дюжину обручей для всего , что связано со ссылками; плюс он редко используется на практике), то же самое для других «современных» языков.
@delnan - извините, я больше знаком с C #, у которого есть такое взаимодействие. Я скорее предположил, что многие из основополагающих библиотек Java также используют JNI внизу.
Теластин
6
Вы приводите хороший аргумент в пользу разрешения null, но вы все равно можете разрешить null, не поощряя его. Скала является хорошим примером этого. Он может беспрепятственно взаимодействовать с Java-API, использующими 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 для «нуля - это легко, параметрические алгебраические типы данных сложнее». Но я думаю, что проблема не столько в параметрической типизации, сколько в ADT; просто они не воспринимаются как необходимые. С другой стороны, если бы Java поставлялась без объектной системы, она бы провалилась; ООП была особенность «демонстрации», потому что если у вас ее не было, никто не интересовался.
Довал
@Doval: хорошо, ООП, возможно, было необходимо для Java, но это было не для C :) Но это правда, что Java стремилась быть простой. К сожалению, люди полагают, что простой язык ведет к простым программам, что довольно странно (Brainfuck - очень простой язык ...), но мы, безусловно, согласны с тем, что сложные языки (C ++ ...) также не являются панацеей, хотя они могут быть невероятно полезными.
Матье М.
1
@MatthieuM .: Реальные системы сложны. Хорошо разработанный язык, сложности которого соответствуют моделируемой реальной системе, может позволить моделировать сложную систему с помощью простого кода. Попытки упростить язык просто наталкивают его на программиста, который его использует.
суперкат
@supercat: я не мог согласиться больше. Или, как перефразировал Эйнштейна: «Сделай все как можно проще, но не проще».
Матье М.
@MatthieuM .: Эйнштейн был мудрым во многих отношениях. Языки, которые пытаются предположить, что «все является объектом, ссылка на который может быть сохранена Object», не в состоянии признать, что практическим приложениям нужны неразделимые изменяемые объекты и разделяемые неизменяемые объекты (оба из которых должны вести себя как значения), а также разделяемые и неразделимые юридические лица. Использование одного Objectтипа для всего не устраняет необходимость в таких различиях; это просто затрудняет их правильное использование.
суперкат
5

Null / nil / none само по себе не является злом.

Если вы посмотрите его вводящее в заблуждение известное выступление «Ошибка в миллиард долларов», Тони Хоар расскажет о том, что позволить любой переменной иметь нулевое значение было огромной ошибкой. Альтернатива - использование Options - фактически не избавляется от нулевых ссылок. Вместо этого он позволяет вам указать, какие переменные могут содержать значение null, а какие нет.

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

Тогда возникает вопрос: почему языки не могут реализовать Опции? На самом деле, пожалуй, самый популярный язык всех времен C ++ имеет возможность определять объектные переменные, которые не могут быть назначены NULL. Это решение «нулевой проблемы», о которой говорил Тони Хоар. Почему следующий самый популярный типизированный язык, Java, не имеет его? Кто-то может спросить, почему у него так много недостатков в целом, особенно в его системе типов. Я не думаю, что вы действительно можете сказать, что языки систематически совершают эту ошибку. Некоторые делают, некоторые нет.

BT
источник
1
Одна из самых сильных сторон Java с точки зрения реализации, но слабая с точки зрения языка, заключается в том, что существует только один не примитивный тип: Promiscuous Object Reference. Это значительно упрощает время выполнения, делая возможными некоторые чрезвычайно легкие реализации JVM. Однако этот дизайн означает, что у каждого типа должно быть значение по умолчанию, а для разнородных ссылок на объекты единственным возможным значением по умолчанию является null.
Суперкат
Ну, во всяком случае , один корень не примитивного типа. Почему это слабость с точки зрения языка? Я не понимаю, почему этот факт требует, чтобы у каждого типа было значение по умолчанию (или наоборот, почему несколько корневых типов позволили бы типам не иметь значения по умолчанию), или почему это слабое место.
BT
Какой другой вид не примитива может содержать элемент поля или массива? Слабым местом является то, что некоторые ссылки используются для инкапсуляции идентичности, а некоторые - для инкапсуляции значений, содержащихся в идентифицированных объектах. Для переменных ссылочного типа, используемых для инкапсуляции идентичности, nullэто единственное разумное значение по умолчанию. Однако ссылки, используемые для инкапсуляции значения, могут иметь разумное поведение по умолчанию в тех случаях, когда тип будет иметь или может создать разумный экземпляр по умолчанию. Многие аспекты того, как должны вести себя ссылки, зависят от того, инкапсулируют ли они ценность и каким образом, но ...
суперкат
... система типов Java не может это выразить. Если fooсодержит единственную ссылку на int[]содержимое, {1,2,3}а код хочет fooсодержать ссылку на int[]содержащее {2,2,3}, самый быстрый способ достичь этого - увеличить foo[0]. Если код хочет дать методу знать, что он fooвыполняется {1,2,3}, другой метод не будет модифицировать массив и не будет сохранять ссылку за пределами точки, в которой fooон хотел бы ее изменить, самый быстрый способ добиться этого - передать ссылку на массив. Если у Java был тип «эфемерная ссылка только для чтения», то ...
суперкат
... массив может быть безопасно передан как эфемерная ссылка, и метод, который хочет сохранить свое значение, будет знать, что ему нужно его скопировать. В отсутствие такого типа единственные способы безопасного раскрытия содержимого массива - это либо сделать его копию, либо инкапсулировать его в объект, созданный только для этой цели.
суперкат
4

Потому что языки программирования обычно разрабатываются, чтобы быть практически полезными, а не технически правильными. Дело в том, что nullсостояния являются обычным явлением из-за плохих или отсутствующих данных или состояния, которое еще не было принято. Технически превосходные решения все более громоздки, чем просто допущение нулевых состояний и смирение с тем фактом, что программисты допускают ошибки.

Например, если я хочу написать простой скрипт, который работает с файлом, я могу написать псевдокод, например:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

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

Джек Эйдли
источник
13
И здесь вы привели пример того, что именно не так с нулями. Правильно реализованная функция «openfile» должна выдавать исключение (для отсутствующего файла), которое немедленно остановит выполнение с точным объяснением того, что произошло. Вместо этого, если он возвращает ноль, он распространяется далее (в for line in file) и выдает бессмысленное исключение нулевой ссылки, что нормально для такой простой программы, но вызывает реальные проблемы отладки в гораздо более сложных системах. Если бы нули не существовали, разработчик openfile не смог бы совершить эту ошибку.
mrpyo
2
+1 за «Потому что языки программирования обычно предназначены для того, чтобы быть практически полезными, а не технически правильными»
Мартин Ба,
2
Каждый тип опций, который я знаю, позволяет вам выполнить сбой по нулю с помощью одного короткого дополнительного вызова метода (пример Rust:) let file = something(...).unwrap(). В зависимости от вашего POV, это простой способ не обрабатывать ошибки или лаконичное утверждение, что null не может возникнуть. Потраченное время минимально, и вы экономите время в других местах, потому что вам не нужно выяснять, может ли что-то быть нулевым. Другое преимущество (которое само по себе может стоить дополнительного вызова) состоит в том, что вы явно игнорируете регистр ошибки; когда это терпит неудачу, есть немного сомнения, что пошло не так и куда нужно исправить.
4
@mrpyo Не все языки поддерживают исключения и / или обработку исключений (а-ля 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 Это хороший момент, но теперь вам не нужно добавлять поддержку для различения неизменяемых и неизменных типов? Я не уверен, насколько банально это добавить к языку.
Довал
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будто они ничего не находят. Хотя в этом случае легко утверждать, что параметр out в первую очередь плохой дизайн, и этот метод должен возвращать различающее объединение или может быть вместо этого.

Все эти проблемы могут быть решены, но это не так просто, как «запретить ноль и все хорошо».

CodesInChaos
источник
Это List<T>ИМХО лучший пример, потому что для каждого из них требуется либо Tзначение по умолчанию, либо для каждого элемента в резервном хранилище, Maybe<T>с дополнительным полем «isValid», даже если Tоно есть Maybe<U>, либо код для List<T>поведения ведет себя по-разному в зависимости на Tсамом ли это обнуляемый тип. Я бы посчитал инициализацию T[]элементов значением по умолчанию наименьшим злом из этих выборов, но это, конечно, означает, что элементы должны иметь значение по умолчанию.
суперкат
Ржавчина следует за пунктом 1 - ноль вообще. Цейлон следует пункту 2 - по умолчанию ненулевой. Ссылки, которые могут быть нулевыми, явно объявляются с типом объединения, который включает в себя либо ссылку, либо нулевой, но ноль никогда не может быть значением простой ссылки. В результате язык полностью безопасен и нет исключения NullPointerException, потому что это семантически невозможно.
Джим Балтер
2

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

Разные языки и системы используют разные подходы.

  • Один из подходов состоит в том, чтобы сказать, что любая попытка прочитать что-то, что не было записано, приведет к немедленной ошибке.

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

  • Третий подход состоит в том, чтобы просто игнорировать проблему и позволить тому, что произойдет "естественно", просто произойти.

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

Подход № 4 намного безопаснее, чем подход № 3, и в целом дешевле, чем подходы № 1 и № 2. Тогда возникает вопрос о том, каким должно быть значение по умолчанию для ссылочного типа. Для неизменяемых ссылочных типов во многих случаях имеет смысл определить экземпляр по умолчанию и сказать, что значением по умолчанию для любой переменной этого типа должна быть ссылка на этот экземпляр. Однако для изменяемых ссылочных типов это было бы не очень полезно. Если предпринята попытка использовать изменяемый ссылочный тип до того, как он будет записан, обычно не существует никаких безопасных действий, кроме как отловить в точке попытки использования.

С семантической точки зрения, если у вас есть массив customersтипа Customer[20], и кто-то пытается, Customer[4].GiveMoney(23)не сохраняя ничего Customer[4], выполнение должно быть перехвачено. Можно утверждать, что попытка чтения Customer[4]должна быть немедленно завершена, а не ждать, пока код попытается это сделать GiveMoney, но есть достаточно случаев, когда полезно прочитать слот, выяснить, что он не содержит значения, а затем использовать это информация о том, что попытка чтения сама по себе не удалась, часто доставляет большие неудобства

Некоторые языки позволяют указывать, что определенные переменные никогда не должны содержать ноль, и любая попытка сохранить ноль должна вызвать немедленную ловушку. Это полезная функция. В целом, тем не менее, любой язык, который позволяет программистам создавать массивы ссылок, должен либо учитывать возможность пустых элементов массива, либо принудительно инициализировать элементы массива данными, которые не могут быть значимыми.

Supercat
источник
Не будет ли Maybe/ Optionтип решить проблему с # 2, так как если вы не имеете значение для вашей справки пока , но будет иметь один в будущем, вы можете просто хранить Nothingв Maybe <Ref type>?
Довал
@Doval: Нет, это не решило бы проблему - по крайней мере, без повторного введения нулевых ссылок. Должно ли «ничто» действовать как член типа? Если да, то какой? Или это должно вызвать исключение? В таком случае, как вам лучше, чем просто nullправильно / разумно использовать?
cHao
@Doval: должен ли тип поддержки List<T>быть T[]или Maybe<T>? А как насчет типа поддержки List<Maybe<T>>?
суперкат
@supercat Я не уверен, как Maybeимеет смысл использовать тип поддержки, Listпоскольку он Maybeсодержит одно значение. Вы имели в виду Maybe<T>[]?
Довал
@cHao Nothingможно назначать только значениям типа Maybe, так что это не совсем то же самое, что присваивание null. Maybe<T>и Tдва разных типа.
Довал