Почему нулевые ссылки избегаются, а выбрасывание исключений считается нормальным?


21

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



2
Некоторые языки обнуляются лучше, чем другие. Для «управляемого кода», такого как .Net / Java, нулевая ссылка - это просто еще один тип проблемы, тогда как другой нативный код может не справиться с этим изящно (вы не упомянули конкретный язык). Даже в управляемом мире иногда вы хотите написать отказоустойчивый код (встроенный?, Оружие?), А иногда вы хотите громко ругаться как можно скорее (модульное тестирование). Оба типа кода могут вызывать одну и ту же библиотеку - это будет проблемой. В целом, я думаю, что код, который пытается не навредить компьютерам, является плохой идеей. В любом случае отказоустойчивость трудна.
Работа

@Job: это пропаганда лени. Если вы знаете, как обрабатывать исключение, вы его обрабатываете. Иногда такая обработка может включать создание другого исключения, но вы никогда не должны позволять исключению с нулевой ссылкой оставаться необработанным. Когда-либо. Это кошмар каждого программиста по обслуживанию; это самое бесполезное исключение во всем дереве. Просто спросите Stack Overflow .
Ааронаут

или, иначе говоря, почему бы не вернуть какой-то код для представления информации об ошибке. Этот аргумент будет бушевать долгие годы.
gbjbaanb

Ответы:


24

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

Что плохо, так это попытка получить доступ к нулевой ссылке (или разыменовать нулевой указатель и т. Д.). Это плохо, потому что это всегда указывает на ошибку; вы никогда бы не сделать что - то подобное на цели, и если вы будете делать это нарочно, то , что еще хуже, потому что это делает его невозможно отличить ожидаемое поведение от багги поведения.

Существуют определенные группы, которые по какой-то причине просто ненавидят концепцию ничтожности, но, как указывает Эд , если у вас ее нет, nullили nilвам просто придется заменить ее чем-то другим, что может привести к чему-то хуже, чем сбой (например, повреждение данных).

Фактически, многие структуры охватывают обе концепции; например, в .NET часто встречающийся шаблон - это пара методов, один из которых начинается с префикса Try(например, TryGetValue). В этом Tryслучае для ссылки устанавливается значение по умолчанию (обычно null), а в другом случае выдается исключение. Нет ничего плохого в любом подходе; оба часто используются в средах, которые их поддерживают.

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

На самом деле очень просто выяснить, какую семантику использовать: имеет ли смысл, когда результат функции не определен? Если это так, вы можете вернуть нулевую ссылку. Если нет, бросьте исключение.


5
На самом деле есть языки, которые не имеют нулевого или нулевого значений и не нуждаются в «замене его чем-то другим». Обнуляемые ссылки подразумевают, что там может быть что-то или нет. Если вы просто требуете, чтобы пользователь явно проверил, есть ли что-то там, вы решили проблему. Смотрите haskell для примера из реальной жизни.
Йоханна Ларссон

3
@ErikKronberg, да, «ошибка в миллиард долларов» и вся эта ерунда, парад людей, пытающихся это проделать и заявляющих, что это свежо и увлекательно, никогда не кончается, поэтому предыдущая ветка комментариев была удалена. Эти революционные замены, которые люди никогда не упускают из виду, всегда являются неким вариантом Нулевого Объекта, Опциона или Контракта, которые на самом деле волшебным образом не устраняют основную логическую ошибку, они просто откладывают или продвигают ее соответственно. В любом случае, это, очевидно, вопрос о языках программирования, которые действительно имеют null, так что на самом деле Haskell здесь не имеет никакого значения.
Aaronaught

1
Вы всерьез утверждаете, что не требовать проверки на нуль - это то же самое, что и требование?
Джоанна Ларссон

3
@ErikKronberg: Да, я «серьезно утверждаю», что необходимость проверки на нуль не особо отличается от (а) необходимости проектирования каждого уровня приложения вокруг поведения нулевого объекта, (б) необходимости сопоставления с шаблоном Варианты все время, или (c) не позволяя вызываемому абоненту сказать «я не знаю» и вызывать исключение или сбой. Есть причина, почему nullона так долго терпела, и большинство людей, которые говорят иначе, кажутся учеными с небольшим опытом разработки реальных приложений с такими ограничениями, как неполные требования или возможная согласованность.
Ааронаут

3
@Aaronaught: Иногда я хотел бы, чтобы была кнопка понижения для комментариев. Нет причин ругать так.
Майкл Шоу,

11

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


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

@Blrfl, в идеале методы довольно короткие, и поэтому легко понять, где именно происходит проблема. Хороший отладчик обычно может хорошо искать исключение с нулевой ссылкой, даже если код длинный. Что мне делать, если я пытаюсь прочитать критический параметр из реестра, которого там нет? Мой реестр испорчен, и я лучше провалю и раздражаю пользователя, чем тихо воссоздаю узел и устанавливаю его по умолчанию. Что если вирус сделал это? Итак, если я получаю нулевое значение, я должен выбросить специализированное исключение или просто позволить ему разорваться? С короткими методами какая разница?
Работа

@Job: Что делать, если у вас нет отладчика? Вы понимаете, что 99,99% времени ваше приложение будет работать в среде релизов? Когда это произойдет, вы захотите использовать более значимое исключение. Возможно, вашему приложению все равно придется отказывать и раздражать пользователя, но, по крайней мере, оно будет выводить отладочную информацию, которая позволит вам быстро отследить проблему, тем самым сводя указанное раздражение к минимуму.
Ааронаут

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

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

8

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

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

Однако на практике вам все равно потребуется какой-то способ указать, инициализирована ли переменная или нет. Тогда у вас будет ошибка, в которой ваша программа не аварийно завершится, а вместо этого продолжит работу, используя какое-то недопустимое значение по умолчанию. Я, честно говоря, не знаю, что лучше. За свои деньги я люблю рано и часто падать.


Рассмотрим неинициализированный подкласс с идентифицирующей строкой, и все его методы генерируют исключения. Если один из них когда-либо появляется, вы знаете, что случилось. На встроенных системах с ограниченной памятью рабочая версия фабрики Uninitialized может просто возвращать ноль.
Джим Балтер

3
@Jim Balter: Полагаю, я не совсем понимаю, как это могло бы помочь на практике. В любой нетривиальной программе вам придется в какой-то момент иметь дело со значениями, которые не могут быть инициализированы. Таким образом, должен быть какой-то способ обозначить значение по умолчанию. Таким образом, вы все равно должны проверить это, прежде чем продолжить. Таким образом, вместо возможного сбоя вы теперь потенциально работаете с неверными данными.
Эд С.

1
Вы также знаете, что произошло, когда вы получили nullвозвращаемое значение: запрошенная вами информация не существует. Там нет разницы. В любом случае, вызывающий объект должен проверить возвращаемое значение, если ему действительно нужно использовать эту информацию для какой-то конкретной цели. Будь то проверка null, нулевой объект или монада, практически не имеет значения.
Ааронаут

1
@Jim Balter: Да, я до сих пор не вижу практической разницы и не понимаю, как это облегчает жизнь людям, пишущим реальные программы за пределами академических кругов. Это не значит, что нет никаких преимуществ, просто то, что они не кажутся мне очевидными.
Эд С.

1
Я объяснил практическое различие с классом Uninitialized дважды - он идентифицирует происхождение нулевого элемента - позволяя точно определить ошибку, когда происходит разыменование нулевого элемента. Что касается языков, разработанных на основе нулевых парадигм, они избегают проблемы с самого начала - они предоставляют альтернативные способы программирования. Это позволяет избежать неинициализированных переменных; это может показаться трудным для понимания, если вы не знакомы с ними. Структурное программирование, которое также позволяет избежать большого количества ошибок, также когда-то считалось «академическим».
Джим Балтер

5

Тони Хоар, который создал идею нулевой ссылки, называет ее ошибкой в ​​миллион долларов .

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

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

Эта проблема особенно проявляется в C или C ++ (например) из-за «жесткой» ошибки, которую она вызывает (немедленное аварийное завершение программы без элегантного восстановления).

На других языках всегда возникает вопрос, как с ними справиться.

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

В Haskell вы должны явно предоставить действие для нулевого случая, и поэтому программисты на Haskell злорадствуют своим коллегам, потому что они поняли это правильно (правильно?).

Это, действительно, старые дебаты о коде ошибки / исключении, но на этот раз со значением Sentinel вместо кода ошибки.

Как всегда, то, что наиболее уместно, зависит от ситуации и семантики, которую вы ищете.


Точно. nullна самом деле это зло, потому что оно подрывает систему типов . (Конечно, у альтернативы есть недостаток в ее многословности, по крайней мере, в некоторых языках.)
Механическая улитка

2

Вы не можете прикрепить понятное человеку сообщение об ошибке к нулевому указателю.

(Однако вы можете оставить сообщение об ошибке в файле журнала.)

В некоторых языках / средах , которые позволяют арифметику указателей, если один из указателей аргумента равно нулю , и это допускается в расчет, то результат был бы недействительным , не нулевой указатель. (*) Больше силы для вас.

(*) Это часто случается в программировании COM , где, если вы попытаетесь вызвать метод интерфейса, но указатель интерфейса будет нулевым, это приведет к вызову неверного адреса, который почти, но не совсем, совершенно не похож на ноль.


2

Возврат NULL (или числового нуля, или логического false) для сигнализации об ошибке неверен как технически, так и концептуально.

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

Концептуально, если вы открываете файл и что-то идет не так, то возвращать значение (даже NULL) будет неправильно. Вам не нужно ничего возвращать, потому что ваша операция не закончилась. Возвращение NULL является концептуальным эквивалентом «Я успешно прочитал файл, и вот что он содержит - ничего». Если это то, что вы хотите выразить (то есть, если NULL имеет смысл как фактический результат для рассматриваемой операции), то непременно верните NULL, но если вы хотите сообщить об ошибке, используйте исключения.

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

Существует также сторона обслуживания проблемы: за исключением, вы должны написать дополнительный код для обработки ошибки; если вы этого не сделаете, программа рано и сильно потерпит неудачу (что хорошо). Если вы возвращаете NULL, чтобы сигнализировать об ошибках, поведение программы по умолчанию - игнорировать ошибку и просто продолжать, пока она не приведет к другим проблемам в будущем - поврежденным данным, segfaults, NullReferenceExceptions, в зависимости от языка. Чтобы сообщить об ошибке рано и громко, вам нужно написать дополнительный код и угадать, что: это та часть, которая не учитывается, когда вы находитесь в сжатые сроки.


1

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

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

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

Большая сложность обычно означает больше ошибок.

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

Что делать с нулевым указателем, когда он обнаружен? Если вы не можете встроить обработку исключительного случая, вы всегда можете вызвать исключение и эффективно делегировать эту обработку специального случая вызывающей стороне. И это именно то, что некоторые языки делают по умолчанию, когда вы разыменовываете нулевой указатель.


1

Специфично для C ++, но там нулевые ссылки избегаются, потому что нулевая семантика в C ++ связана с типами указателей. Вполне разумно, чтобы функция открытия файла не работала и возвращала нулевой указатель; на самом деле fopen()функция делает именно это.


Действительно, если вы обнаружите, что в C ++ есть нулевая ссылка, это потому, что ваша программа уже сломана .
Kaz Dragon

1

Это зависит от языка.

Например, Objective-C позволяет вам отправлять сообщение нулевому (nil) объекту без проблем. Вызов nil также возвращает nil и считается особенностью языка.

Мне лично это нравится, поскольку вы можете положиться на это поведение и избегать всех этих замысловатых вложенных if(obj == null)конструкций.

Например:

if (myObject != nil && [myObject doSomething])
{
    ...
}

Можно сократить до:

if ([myObject doSomething])
{
    ...
}

Короче говоря, это делает ваш код более читабельным.


0

Я не буду кричать, возвращаете ли вы нулевое значение или выбрасываете исключение, если вы его документируете.


А также назовите это: GetAddressMaybeили GetSpouseNameOrNull.
Руонг

-1

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

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


2
Нулевая ссылка может «возникать» по множеству причин, и очень немногие из них относятся к чему-то пропущенному. Возможно, вы также путаете его с исключением нулевой ссылки .
Aaronaught

-1

Пустые ссылки часто очень полезны: например, элемент в связанном списке может иметь преемника или не иметь преемника. Использование слова null«нет преемника» совершенно естественно. Или, у человека может быть супруг или нет - использование слова null«человек не имеет супруга» является совершенно естественным, гораздо более естественным, чем наличие некоторого «не имеет супруга» - особая ценность, на которую Person.Spouseможет ссылаться член.

Но: многие значения не являются обязательными. В типичной ООП-программе я бы сказал, что более половины ссылок не может быть nullпосле инициализации, иначе программа потерпит неудачу. В противном случае код должен быть пронизан if (x != null)проверками. Так почему каждая ссылка должна быть обнуляемой по умолчанию? На самом деле все должно быть наоборот: переменные по умолчанию должны быть не обнуляемыми, и вы должны явно сказать «о, и это значение nullтоже может быть ».


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

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

-1

Ваш вопрос сформулирован до смешного. Вы имеете в виду исключение нулевой ссылки (которое на самом деле вызвано попыткой дессылка нулевая)? Явная причина не желать такого типа исключений состоит в том, что он не дает никакой информации о том, что пошло не так, или даже когда - значение могло быть установлено равным нулю в любой точке программы. Вы пишете «Если я запрашиваю доступ для чтения к файлу, который не существует, я совершенно счастлив получить исключение или нулевую ссылку» - но вы не должны быть совершенно счастливы получить что-то, что не указывает на причину , Нигде в строке «попытка разыменования null» не упоминается о чтении или несуществующих файлах. Возможно, вы имеете в виду, что вы были бы совершенно счастливы получить нулевое значение в качестве возвращаемого значения от вызова чтения - это совсем другой вопрос, но он все еще не дает вам никакой информации о том, почему чтение не удалось; Это'


Нет, я не имею в виду исключение нулевой ссылки. На всех языках, которые я знаю, неинициализированные, но объявленные переменные являются формой null.
davidk01

Но ваш вопрос был не о неинициализированных переменных. И есть языки, которые не используют null для неинициализированных переменных, но вместо этого используют объекты-обертки, которые могут дополнительно содержать значение - например, Scala и Haskell.
Джим Балтер

1
«... но вместо этого используйте объекты-обертки, которые могут содержать значение ». Что явно не похоже на обнуляемый тип .
Aaronaught

1
В haskell нет такой вещи как неинициализированная переменная. Вы могли бы потенциально объявить IORef и заполнить его None в качестве начального значения, но это в значительной степени аналог объявления переменной на каком-то другом языке и оставления ее неинициализированной, что влечет за собой все те же проблемы. Работая в чисто функциональном ядре вне IO-монады, программисты на Haskell не обращаются к ссылочным типам, поэтому нет проблем с нулевыми ссылками.
davidk01

1
Если у вас есть исключительное значение «Отсутствует», это точно так же, как исключительное «нулевое» значение - если у вас есть те же инструменты, доступные для его обработки. Это значительное «если». Вам нужна дополнительная сложность, чтобы справиться с этим делом в любом случае. В Haskell сопоставление с образцом и система типов предоставляют способ справиться с этой сложностью. Однако существуют другие инструменты для управления сложностью на других языках. Исключения являются одним из таких инструментов.
Steve314
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.