Это одно из правил, которые повторяются снова и снова, и это сбивает меня с толку.
Нули - это зло, и их следует избегать всякий раз, когда это возможно .
Но, но - из-за моей наивности, позвольте мне закричать - иногда значение МОЖЕТ быть значимо отсутствовать!
Пожалуйста, позвольте мне спросить об этом на примере, взятом из этого ужасного кода с анти-паттернами, над которым я сейчас работаю . По сути, это многопользовательская пошаговая веб-игра, в которой ходы обоих игроков выполняются одновременно (как в покемонах и в противоположность шахматам).
После каждого хода сервер транслирует список обновлений JS-кода на стороне клиента. Класс обновления:
public class GameUpdate
{
public Player player;
// other stuff
}
Этот класс сериализуется в JSON и отправляется подключенным игрокам.
У большинства обновлений, естественно, есть связанный с ними игрок - в конце концов, необходимо знать, какой игрок сделал ход на этом ходу. Однако некоторые обновления не могут иметь игрока, значимо связанного с ними. Пример: игра была принудительно связана, потому что предел хода без действий был превышен. Я думаю, что для таких обновлений имеет смысл сделать игрока обнуленным.
Конечно, я мог бы «исправить» этот код, используя наследование:
public class GameUpdate
{
// stuff
}
public class GamePlayerUpdate : GameUpdate
{
public Player player;
// other stuff
}
Однако я не вижу, как это улучшится, по двум причинам:
- Код JS теперь просто получит объект без Player в качестве определенного свойства, которое будет таким же, как если бы оно было нулевым, поскольку оба случая требуют проверки, присутствует ли значение или отсутствует;
- Если к классу GameUpdate будет добавлено еще одно обнуляемое поле, мне придется иметь возможность использовать множественное наследование для продолжения этого дизайна - но MI само по себе зло (по мнению более опытных программистов) и, что более важно, C # не делает так что я не могу его использовать.
У меня есть подозрение, что этот фрагмент этого кода - один из очень многих, где опытный, хороший программист кричал бы в ужасе. В то же время я не могу понять, как в этом месте это ноль вредит чему-либо и что следует делать вместо этого.
Не могли бы вы объяснить мне эту проблему?
источник
Ответы:
Много вещей лучше вернуть, чем ноль.
(именно это заставило вас считать нулевым в первую очередь)
Хитрость заключается в том, чтобы понять, когда это сделать. Null действительно хорош для того, чтобы взорваться на твоем лице, но только тогда, когда ты усеиваешь его, не проверяя его. Нехорошо объяснять почему.
Если вам не нужно взрываться, и вы не хотите проверять на ноль, используйте одну из этих альтернатив. Может показаться странным возвращать коллекцию, если в ней только 0 или 1 элемент, но она действительно хороша, чтобы позволить вам молча справиться с пропущенными значениями.
Монады звучат причудливо, но здесь все, что они делают, это позволяют вам показать, что действительные размеры для коллекции - 0 и 1. Если они у вас есть, рассмотрите их.
Любой из них лучше прояснит ваши намерения, чем нулевой. Это самое важное отличие.
Это может помочь понять, почему вы вообще возвращаетесь, а не просто выбросить исключение. Исключения - это, по сути, способ отклонить предположение (это не единственный способ). Когда вы спрашиваете о точке пересечения двух линий, вы предполагаете, что они пересекаются. Они могут быть параллельными. Вы должны бросить исключение "параллельных линий"? Ну, вы могли бы, но теперь вы должны обработать это исключение в другом месте или позволить ему остановить систему.
Если вы предпочитаете оставаться там, где вы есть, вы можете превратить отклонение этого предположения в своего рода ценность, которая может выражать либо результаты, либо отклонения. Это не так странно. Мы делаем это в алгебре все время. Хитрость заключается в том, чтобы сделать это значение значимым, чтобы мы могли понять, что произошло.
Если возвращаемое значение может выражать результаты и отклонения, его необходимо отправить в код, который может обрабатывать оба. Полиморфизм действительно мощный для использования здесь. Вместо того, чтобы просто торговать нулевыми чеками для
isPresent()
чеков, вы можете написать код, который ведет себя хорошо в любом случае. Либо заменяя пустые значения значениями по умолчанию, либо молча ничего не делая.Проблема с нулем в том, что это может означать слишком много вещей. Так что это в конечном итоге уничтожает информацию.
В моем любимом нулевом мысленном эксперименте я прошу вас представить сложную машину Рубе Голдберга, которая сигнализирует закодированные сообщения, используя цветной свет, подбирая цветные лампочки с конвейерной ленты, завинчивая их в розетку и включая их. Сигнал контролируется разными цветами лампочек, которые размещены на ленточном конвейере.
Вас попросили сделать эту ужасно дорогую вещь совместимой с RFC3.14159, в которой говорится, что между сообщениями должен быть маркер. Маркер не должен светиться вообще. Это должно длиться ровно один импульс. То же количество времени, что обычно светит один цветной свет.
Вы не можете просто оставлять пробелы между сообщениями, потому что хитрое устройство вызывает тревоги и останавливается, если не может найти лампочку для вставки в розетку.
Каждый, кто знаком с этим проектом, содрогается при прикосновении к машине или к ее контрольному коду. Это не легко изменить. Чем ты занимаешься? Ну, вы можете погрузиться и начать ломать вещи. Может быть, обновить эти вещи отвратительный дизайн. Да, ты мог бы сделать это намного лучше. Или вы можете поговорить с дворником и начать собирать сгоревшие лампочки.
Это полиморфное решение. Системе даже не нужно знать, что что-то изменилось.
Это действительно хорошо, если вы можете осуществить это. Кодируя осмысленное отклонение предположения в значение, вам не нужно заставлять вопрос меняться.
Сколько яблок ты мне должен, если я дам тебе 1 яблоко? -1. Потому что я был должен вам 2 яблока со вчерашнего дня. Здесь предположение вопроса «ты мне должен» отвергается с отрицательным числом. Несмотря на то, что вопрос был неправильным, значимая информация закодирована в этом ответе. Отвергая это осмысленно, вопрос не заставляется меняться.
Тем не менее, иногда изменение вопроса на самом деле путь. Если предположение можно сделать более надежным, но все же полезным, подумайте об этом.
Ваша проблема в том, что обычно обновления приходят от игроков. Но вы обнаружили необходимость в обновлении, которое не приходит от игрока 1 или игрока 2. Вам нужно обновление, которое говорит, что время истекло. Ни один из игроков не говорит этого, так что вы должны делать? Оставить игрока нулевым кажется таким соблазнительным. Но это уничтожает информацию. Вы пытаетесь закодировать знания в черной дыре.
Игровое поле не говорит вам, кто только что переехал. Вы думаете, что это так, но эта проблема доказывает, что это ложь. Поле игрока сообщает вам, откуда пришло обновление. Ваше время истекло, обновление приходит с игровых часов. Я рекомендую дать часам кредит, который они заслуживают, и изменить имя
player
поля на что-то вродеsource
.Таким образом, если сервер выключается и должен приостановить игру, он может отправить обновление, в котором сообщается о происходящем и указывается в качестве источника обновления.
Значит ли это, что вы никогда не должны просто что-то упускать? Нет. Но вы должны учитывать, насколько хорошо можно предположить, что на самом деле ничего не означает единственное хорошо известное хорошее значение по умолчанию. Ничто не является действительно простым в использовании. Так что будьте осторожны при использовании. Существует школа мысли, которая не поддерживает ничего . Это называется соглашением по конфигурации .
Мне это нравится самому, но только тогда, когда об этом договоре ясно сообщают. Это не должно быть способом смутить новичков. Но даже если вы используете это, null все еще не лучший способ сделать это. Нуль - это опасное ничто . Null притворяется чем-то, что вы можете использовать до тех пор, пока вы его не используете, а затем пробивает дыру во вселенной (разрушает вашу семантическую модель). Если вам нужно проделать дыру во вселенной, то какое поведение вам нужно, почему вы возитесь с этим? Используйте что-то еще.
источник
null
это на самом деле не проблема здесь. Проблема в том, как большинство современных языков программирования справляются с отсутствующей информацией; они не воспринимают эту проблему всерьез (или, по крайней мере, не обеспечивают поддержку и безопасность, которые необходимы большинству землян, чтобы избежать ловушек). Это приводит ко всем проблемам, которые мы научились бояться (Javas NullPointerException, Segfaults, ...).Давайте посмотрим, как решить эту проблему лучше.
Другие предложили шаблон нулевого объекта . Хотя я думаю, что иногда это применимо, существует множество сценариев, в которых просто не существует значения по умолчанию, подходящего всем. В этих случаях потребители потенциально отсутствующей информации должны быть в состоянии решить для себя, что делать, если эта информация отсутствует.
Существуют языки программирования, которые обеспечивают безопасность от нулевых указателей (так называемая безопасность void) во время компиляции. Приведите несколько современных примеров: Kotlin, Rust, Swift. В этих языках проблема недостающей информации воспринималась всерьез, и использование null там совершенно безболезненно - компилятор остановит вас от разыменования нулевых указателей.
В Java были предприняты усилия для создания аннотаций @ Nullable / @ NotNull вместе с плагинами компилятора + IDE для достижения того же эффекта. Это не было действительно успешно, но это выходит за рамки для этого ответа.
Явный универсальный тип, обозначающий возможное отсутствие, является еще одним способом решения этой проблемы. Например на Яве есть
java.util.Optional
. Это может работать:на уровне проекта или компании вы можете установить правила / руководящие принципы
java.util.Optional
вместоnull
Ваше языковое сообщество / экосистема, возможно, придумали другие способы решения этой проблемы лучше, чем компилятор / интерпретатор.
источник
Optional.isPresent
isPresent()
не является рекомендуемым использованием OptionalЯ не вижу никакой пользы в утверждении, что нуль - это плохо. Я проверил обсуждения по этому поводу, и они только делают меня нетерпеливым и раздраженным.
Нулевое значение может быть использовано неправильно, как «магическое значение», когда оно действительно что-то значит. Но вам нужно было бы сделать довольно запутанное программирование, чтобы попасть туда.
Нулевое значение должно означать «не там», то есть не создано (пока) или (уже) ушло или не предоставлено, если это аргумент. Это не государство, все это отсутствует. В ОО-настройке, где вещи создаются и уничтожаются все время, что является допустимым, значимым условием, отличным от любого состояния.
Не совсем верно, я могу получить предупреждение о том, что не проверять нулевое значение перед использованием переменной. И это не то же самое. Возможно, я не хочу щадить ресурсы объекта, который не имеет цели. И что еще более важно, мне придется проверить альтернативу так же, чтобы получить желаемое поведение. Если я забуду это сделать, я скорее получу исключение NullReferenceException во время выполнения, чем какое-то неопределенное / непреднамеренное поведение.
Есть одна проблема, о которой нужно знать. В многопоточном контексте вы должны были бы защититься от условий гонки, присваивая локальную переменную, прежде чем выполнять проверку на ноль.
Нет, это ничего не значит, поэтому нечего разрушать. Имя и тип переменной / аргумента всегда скажут, чего не хватает, это все, что нужно знать.
Редактировать:
Я на полпути к видео candied_orange, связанному в одном из его других ответов о функциональном программировании, и оно очень интересно. Я вижу полезность этого в определенных сценариях. Функциональный подход, который я видел до сих пор, с кусками кода, летящими вокруг, обычно заставляет меня съеживаться при использовании в настройках OO. Но я думаю, это имеет свое место. Где-то. И я предполагаю, что будут языки, которые хорошо справляются со своей задачей, скрывая то, что я воспринимаю как запутанный беспорядок, когда сталкиваюсь с этим в C #, языки, которые вместо этого предоставляют чистую и работоспособную модель.
Если эта функциональная модель является вашим мышлением / структурой, то на самом деле нет альтернативы продвижению каких-либо неудач вперед в виде документированных описаний ошибок и, в конечном счете, разрешению вызывающей стороне справиться с накопленным мусором, который тянулся по пути назад до точки инициации. Видимо, именно так работает функциональное программирование. Назовите это ограничением, назовите это новой парадигмой. Вы можете считать это хаком для функциональных учеников, который позволяет им идти дальше по нечестивому пути немного дальше, вы можете быть восхищены этим новым способом программирования, который открывает новые захватывающие возможности. Как бы то ни было, мне кажется бессмысленным вступать в этот мир, возвращаться к традиционному ОО-способу ведения дел (да, мы можем выбросить исключения, извините), выбрать из него какую-то концепцию,
Любая концепция может быть использована неправильно. С моей точки зрения, представленные «решения» не являются альтернативой для правильного использования нуля. Это понятия из другого мира.
источник
None
Представляет ли ….Ваш вопрос.
Полностью на борту с вами там. Тем не менее, есть несколько предостережений, о которых я расскажу через секунду. Но сначала:
Я думаю, что это благими намерениями, но чрезмерно обобщенное заявление.
Нули не являются злом. Они не антипаттерн. Их нельзя избегать любой ценой. Тем не менее, нулевые значения подвержены ошибкам (из-за невозможности проверки на нулевые значения).
Но я не понимаю, как отказ от проверки на ноль является каким-то доказательством того, что нуль по своей сути неправильно использовать.
Если null является злом, то то же самое относится и к индексам массивов и указателям на C ++. Они не. С ними легко выстрелить себе в ногу, но их не следует избегать любой ценой.
В остальной части этого ответа я собираюсь адаптировать намерение «нули - это зло» к более нюансам « нули должны обрабатываться ответственно ».
Ваше решение
Хотя я согласен с вашим мнением об использовании null как глобального правила; Я не согласен с тем, что вы используете null в данном конкретном случае.
Подводя итог приведенным ниже отзывам: вы неправильно понимаете цель наследования и полиморфизма. В то время как ваш аргумент в пользу использования null имеет смысл, ваше дальнейшее «доказательство» того, почему другой путь плохой, основано на неправильном использовании наследования, что делает ваши точки несколько не относящимися к нулевой проблеме.
Если эти обновления значимо отличаются (какими они являются), то вам нужны два отдельных типа обновлений.
Рассмотрим сообщения, например. У вас есть сообщения об ошибках и сообщения чата. Но хотя они могут содержать очень похожие данные (строковое сообщение и отправитель), они не ведут себя одинаково. Их следует использовать отдельно, как разные классы.
То, что вы делаете сейчас, - это эффективная дифференциация между двумя типами обновлений, основанная на нулевом свойстве Player. Это эквивалентно выбору между сообщением IM и сообщением об ошибке на основе наличия кода ошибки. Это работает, но это не лучший подход.
Ваш аргумент опирается на ошибки полиморфизма. Если у вас есть метод, который возвращает
GameUpdate
объект, он вообще не должен делать различий междуGameUpdate
,PlayerGameUpdate
илиNewlyDevelopedGameUpdate
.Базовый класс должен иметь полностью рабочий контракт, то есть его свойства определены в базовом классе, а не в производном классе (при этом значение этих базовых свойств, конечно, может быть переопределено в производных классах).
Это делает ваш аргумент спорным. В хорошей практике вы никогда не должны заботиться о том, что на вашем
GameUpdate
объекте есть или нет игрока. Если вы пытаетесь получить доступ кPlayer
свойствуGameUpdate
, то вы делаете что-то нелогичное.Типизированные языки, такие как C #, даже не позволят вам получить доступ к
Player
свойству a,GameUpdate
потому чтоGameUpdate
просто не имеют этого свойства. Javascript значительно слабее в своем подходе (и не требует предварительной компиляции), поэтому он не пытается исправить вас и взорвет его во время выполнения.Вы не должны создавать классы, основанные на добавлении обнуляемых свойств. Вы должны создавать классы на основе функционально уникальных типов обновлений игры .
Как избежать проблем с нулем вообще?
Я думаю, что важно уточнить, как вы можете осмысленно выразить отсутствие ценности. Это набор решений (или плохих подходов) в произвольном порядке.
1. В любом случае используйте нуль.
Это самый простой подход. Тем не менее, вы подписываетесь на тонну нулевых проверок и (в случае неудачи) устраняете исключительные ситуации нулевых ссылок.
2. Используйте ненулевое бессмысленное значение
Простой пример здесь
indexOf()
:Вместо того
null
, чтобы получить-1
. На техническом уровне это допустимое целочисленное значение. Однако отрицательное значение является бессмысленным ответом, так как ожидается, что индексы будут положительными целыми числами.По логике это можно использовать только в тех случаях, когда возвращаемый тип допускает больше возможных значений, чем может быть возвращено осмысленно (indexOf = положительные целые числа; int = отрицательные и положительные целые числа)
Для ссылочных типов вы все равно можете применить этот принцип. Например, если у вас есть объект с целочисленным идентификатором, вы можете вернуть фиктивный объект с идентификатором, установленным на -1, в отличие от нуля.
Вам просто нужно определить «фиктивное состояние», с помощью которого вы можете измерить, является ли объект действительно пригодным для использования или нет.
Обратите внимание, что вы не должны полагаться на предопределенные строковые значения, если вы не контролируете строковое значение. Возможно, вы захотите установить имя пользователя на «ноль», если хотите вернуть пустой объект, но у вас возникнут проблемы, когда Кристофер Налл подпишется на ваш сервис .
3. Бросьте исключение вместо.
Нет, просто нет. Исключения не должны обрабатываться для логического потока.
При этом в некоторых случаях вы не хотите скрывать исключение (nullreference), а вместо этого показывать соответствующее исключение. Например:
Это может выдать
PersonNotFoundException
(или аналогичный), когда нет человека с идентификатором 123.Совершенно очевидно, что это приемлемо только в тех случаях, когда отсутствие предмета делает невозможным дальнейшую обработку. Если не удается найти элемент, и приложение может продолжаться, то НИКОГДА не следует выбрасывать исключение . Я не могу подчеркнуть это достаточно.
источник
Причина, по которой нулевые значения являются плохими, заключается не в том, что отсутствующее значение кодируется как нулевое, а в том, что любое ссылочное значение может быть нулевым Если у вас есть C #, вы, вероятно, все равно потерялись. Я не вижу, чтобы точка ro использовала там какой-либо необязательный параметр, поскольку ссылочный тип уже является дополнительным. Но для Java есть аннотации @Notnull, которые, кажется, вы можете настроить на уровне пакета , тогда для значений, которые могут быть пропущены, вы можете использовать аннотацию @Nullable.
источник
Нули не являются злом, практически невозможно реализовать какую-либо альтернативу, не имея дело с чем-то, что выглядит как ноль.
Нули однако опасны . Как и в случае с любой другой низкоуровневой конструкцией, если вы ошиблись, ниже ничего не поймать.
Я думаю, что есть много веских аргументов против нуля:
Что, если вы уже находитесь в домене высокого уровня, существуют более безопасные шаблоны, чем при использовании модели низкого уровня.
Если вам не нужны преимущества низкоуровневой системы и вы готовы быть осторожными, то, возможно, вам не следует использовать низкоуровневый язык.
и т.д ...
Все они действительны, и, кроме того, не нужно прилагать усилия для написания геттеров, сеттеров и т. Д., Как общая причина не следовать этому совету. Неудивительно, что многие программисты пришли к выводу, что нулевые значения опасны и ленивы (плохая комбинация!), Но, как и многие другие «универсальные» советы, они не так универсальны, как они сформулированы.
Хорошие люди, которые написали язык, думали, что они будут кому-то полезны. Если вы понимаете возражения и по-прежнему думаете, что они вам подходят, больше никто не узнает.
источник
У Java есть
Optional
класс с явнымifPresent
+ лямбда для безопасного доступа к любому значению. Это можно сделать и в JS:Смотрите этот вопрос StackOverflow .
Кстати, многим пуристам это использование покажется сомнительным, но я так не думаю. Дополнительные функции существуют.
источник
java.lang.Optional
бытьnull
тоже?Optional.of(null)
выдаст исключение,Optional.ofNullable(null)
выдастOptional.empty()
значение not-there.Optional<Type> a = null
?Optional<Optional<Type>> a = Optional.empty();
;) - Другими словами, в JS (и других языках) такие вещи будут невозможны. Scala со значениями по умолчанию будет делать. Ява может быть с перечислением. Я сомневаюсьOptional<Type> a = Optional.empty();
.null
или пустого к двум плюс некоторым дополнительным методам на вставленном объекте. Это настоящее достижение.CAR Hoare назвал нуль своей «ошибкой в миллиард долларов». Тем не менее, важно отметить, что он думал, что решение было не «нигде не имеет значения null», а вместо этого «ненулевые указатели как значения по умолчанию, и null только там, где они семантически необходимы».
Проблема с нулем в языках, которые повторяют его ошибку, состоит в том, что вы не можете сказать, что функция ожидает ненулевые данные; это в принципе невыразимо в языке. Это заставляет функции включать в себя множество бесполезных шаблонов обработки лишних данных, и забвение нулевой проверки является распространенным источником ошибок.
Чтобы обойти это фундаментальное отсутствие выразительности, некоторые сообщества (например, сообщество Scala) не используют нуль. В Scala, если вам нужно значение типа
T
, допускающее обнуляемость , вы берете аргумент типаOption[T]
, а если вам нужно значение, не допускающее обнуления, вы просто принимаете aT
. Если кто-то пропустит нуль в ваш код, ваш код взорвется. Однако считается, что вызывающий код является ошибочным кодом.Option
Это довольно хорошая замена для нуля, потому что общие шаблоны обработки нуля извлекаются в библиотечные функции, такие как.map(f)
или.getOrElse(someDefault)
.источник