Является ли новое булево поле лучше нулевой ссылки, когда значение может отсутствовать?

39

Например, предположим, у меня есть класс Member, который имеет lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

у которого lastChangePasswordTime может отсутствовать, потому что некоторые участники никогда не могут менять свои пароли.

Но в соответствии с тем, что пустые значения являются злом, что следует использовать, когда значение может отсутствовать по значению? и https://softwareengineering.stackexchange.com/a/12836/248528 , я не должен использовать null для представления значимо отсутствующего значения. Поэтому я пытаюсь добавить логический флаг:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Но я думаю, что это довольно устарело, потому что:

  1. Когда isPasswordChanged имеет значение false, lastChangePasswordTime должно иметь значение null, а проверка lastChangePasswordTime == null практически идентична проверке isPasswordChanged - false, поэтому я предпочитаю проверить lastChangePasswordTime == null напрямую

  2. При изменении логики здесь я могу забыть обновить оба поля.

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

this.lastChangePasswordTime=Date.now();

Является ли дополнительное булево поле лучше нулевой ссылки здесь?

ocomfd
источник
51
Этот вопрос зависит от языка, потому что хорошее решение во многом зависит от того, что предлагает ваш язык программирования. Например, в C ++ 17 или Scala вы бы использовали std::optionalили Option. В других языках вам, возможно, придется создать соответствующий механизм самостоятельно, или вы можете прибегнуть к nullчему-то подобному, потому что это более идиоматично.
Кристиан Хакл
16
Есть ли причина, по которой вы не хотите, чтобы lastChangePasswordTime устанавливал время создания пароля (в конце концов, создание - это мод)?
Кристиан Х
@ChristianHackl хм, согласен, что есть разные «идеальные» решения, но я не вижу никакого (основного) языка, хотя в общем случае использование отдельного логического выражения было бы лучше, чем выполнение нулевых / нулевых проверок. Не совсем уверен насчет C / C ++, так как я давно там не работал.
Фрэнк Хопкинс
@FrankHopkins: Примером могут служить языки, в которых переменные можно неинициализировать, например, C или C ++. lastChangePasswordTimeтам может быть неинициализированный указатель, и сравнение его с чем угодно будет неопределенным поведением. Не очень убедительная причина не инициализировать указатель на NULL/ nullptrвместо, особенно не в современном C ++ (где вы вообще не используете указатель), но кто знает? Другим примером могут быть языки без указателей или с плохой поддержкой указателей, возможно. (ФОРТРАН 77 приходит на ум ...)
Кристиан Хакл
Для этого я бы использовал enum с 3 случаями :)
J. Doe

Ответы:

72

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

Если ваша цель заключается в том, чтобы заключить значение Nullable во избежание случайного обращения к нему, я бы предложил создать isPasswordChangedзначение в виде функции или свойства, которое возвращает результат нулевой проверки, например:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

На мой взгляд, делать это так:

  • Дает лучшую читаемость кода, чем нулевая проверка, которая может потерять контекст.
  • Устраняет необходимость беспокоиться о сохранении упомянутой isPasswordChangedвами ценности.

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

TZHX
источник
29
+1 за открытие. Бессмысленное использование нулевого значения, как правило, не одобряется только в тех случаях, когда нулевое значение является неожиданным результатом, т. Е. Когда оно не имеет смысла (для потребителя). Если ноль имеет смысл, то это не неожиданный результат.
Флейтер
28
Я бы сказал, что это немного сильнее, так как никогда не бывает двух переменных, которые поддерживают одно и то же состояние. Это не удастся. Сокрытие нулевого значения в предположении, что нулевое значение имеет смысл внутри экземпляра, извне - очень хорошая вещь. В этом случае, когда вы можете позже решить, что 1970-01-01 означает, что пароль никогда не менялся. Тогда на логику остальной программы наплевать.
согнуло
3
Вы можете забыть позвонить так isPasswordChangedже, как вы можете забыть, чтобы проверить, если это ноль. Я не вижу здесь ничего полезного.
Герман
9
@Gherman Если потребитель не понимает, что значение null является значимым или ему необходимо проверить наличие значения, он теряется в любом случае, но если метод используется, всем, кто читает код, понятно, почему это проверка, и есть разумная вероятность того, что значение является нулевым / не присутствует. В противном случае неясно, был ли это просто разработчик, добавивший нулевую проверку просто «почему бы и нет», или это часть концепции. Конечно, это можно выяснить, но один способ получить информацию напрямую, а другой - изучить реализацию.
Фрэнк Хопкинс
2
@FrankHopkins: Хороший вопрос, но если используется параллелизм, даже проверка одной переменной может потребовать блокировки.
Кристиан Хакл
63

nullsне злые Использование их без размышлений есть. Это тот случай, когда nullточно правильный ответ - даты нет.

Обратите внимание, что ваше решение создает больше проблем. Что означает дата, установленная для чего-то, но isPasswordChangedложная? Вы только что создали случай противоречивой информации, которую нужно специально отловить и обработать, в то время как nullзначение имеет четко определенное, однозначное значение и не может противоречить другой информации.

Так что нет, ваше решение не лучше. Принятие nullзначения является правильным подходом здесь. Люди, которые утверждают, что nullэто всегда зло, независимо от контекста, не понимают, почему nullсуществует.

Том
источник
17
Исторически, nullсуществует, потому что Тони Хоар допустил ошибку в миллиард долларов в 1965 году. Люди, которые утверждают, что nullэто «зло», конечно, слишком упрощают тему, но хорошо, что современные языки или стили программирования отходят null, заменяя это с выделенными типами опций.
Кристиан Хакл
9
@ChristianHackl: просто из исторического интереса - умрет ли когда-нибудь Хоар, какой была бы лучшая практическая альтернатива (без перехода на совершенно новую парадигму)?
AnoE
5
@AnoE: интересный вопрос. Не знаю, что Хоар должен был сказать по этому поводу, но в наши дни программирование без нуля часто становится реальностью в современных, хорошо разработанных языках программирования.
Кристиан Хакл
10
@ Том Ват? В таких языках, как C # и Java, DateTimeполе является указателем. Вся концепция nullимеет смысл только для указателей!
Тодд Сьюэлл
16
@Tom: нулевые указатели опасны только для языков и реализаций, которые позволяют их слепо индексировать и разыменовывать, с какой бы то ни было веселостью. В языке, который прерывает попытки индексировать или разыменовывать нулевой указатель, он часто будет менее опасным, чем указатель на фиктивный объект. Во многих ситуациях предпочтительнее иметь ненормальное завершение программы, чем создавать бессмысленные числа, и в системах, которые их перехватывают, нулевые указатели являются правильным рецептом для этого.
суперкат
29

В зависимости от вашего языка программирования, могут быть хорошие альтернативы, такие как optionalтип данных. В C ++ (17) это будет std :: необязательный . Он может отсутствовать или иметь произвольное значение базового типа данных.

dasmy
источник
8
Там тоже Optionalв яве; смысл ноль Optionalбыть на ваше
усмотрение
1
Пришел сюда, чтобы соединиться с Факультативным. Я бы закодировал это как тип в языке, который их имел.
Зипп
2
@FrankHopkins Жаль, что ничто в Java не мешает вам писать Optional<Date> lastPasswordChangeTime = null;, но я бы не сдался только из-за этого. Вместо этого я хотел бы привить очень твердое командное правило, согласно которому никакие необязательные значения никогда не могут быть назначены null. Факультативно покупает слишком много приятных функций, чтобы легко отказаться от них. Вы можете легко назначить значения по умолчанию ( orElseили с ленивым вычислением:) orElseGet, выбросить ошибки ( orElseThrow), преобразовать значения в нулевой безопасный способ ( map, flatmap) и т. Д.
Александр - Восстановить Монику
5
По моему опыту, проверка @FrankHopkins isPresent- это почти всегда запах кода и довольно надежный признак того, что разработчики, использующие эти опции, «упускают суть». На самом деле, это не дает никакой пользы ref == null, но вы также не должны часто пользоваться этим. Даже если команда нестабильна, инструмент статического анализа может установить закон, такой как linter или FindBugs , который может поймать большинство случаев.
Александр - Восстановить Монику
5
@FrankHopkins: Может случиться так, что сообществу Java просто нужно немного изменить парадигму, что может занять много лет. Если посмотреть на Scala, например, он поддерживает , nullпотому что язык является 100% совместим с Java, но никто не использует его, и Option[T]это повсюду, с отличной функциональной программирования поддержки , как map, flatMap, сопоставления с образцом и так далее, который идет далеко, далеко за пределы == nullпроверок. Вы правы в том, что в теории тип параметра звучит немного больше, чем прославленный указатель, но реальность доказывает, что этот аргумент неверен.
Кристиан Хакл
11

Правильно используя нуль

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

В этих ситуациях вы в основном используете это так (в псевдокоде):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

проблема

И это очень большая проблема. Проблема в том, что к тому времени, когда вы вызываете doSomethingUsefulзначение, возможно, не было проверено null! Если этого не произошло, программа, скорее всего, вылетит. И пользователь может даже не увидеть никаких хороших сообщений об ошибках, оставаясь с чем-то вроде «ужасная ошибка: хотел значение, но получил ноль!» (после обновления: хотя может быть даже менее информативная ошибка, например Segmentation fault. Core dumped., или, что еще хуже, в некоторых случаях нет ошибок и неправильных манипуляций с нулем)

Забыть писать чеки nullи обрабатывать nullситуации - очень распространенная ошибка . Вот почему изобретатель Тони Хоар nullсказал на конференции по программному обеспечению QCon London в 2009 году, что он допустил ошибку в миллиард долларов в 1965 году: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Ошибка-Тони Хоара

Как избежать проблемы

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

Например, Haskell имеет Maybeмонаду вместо нулей. Предположим, что DatabaseRecordэто определенный пользователем тип. В Haskell значение типа Maybe DatabaseRecordможет быть равным Just <somevalue>или равным Nothing. Затем вы можете использовать его по-разному, но независимо от того, как вы его используете, вы не сможете применить некоторые операции, Nothingне зная об этом.

Например, эта вызываемая функция zeroAsDefaultвозвращает xдля Just xи 0для Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Кристиан Хакл говорит, что C ++ 17 и Scala имеют свои собственные пути. Поэтому вы можете попытаться выяснить, есть ли на вашем языке что-то подобное, и использовать его.

Нули все еще широко используются

Если у вас нет ничего лучше, тогда nullпользуйтесь. Просто продолжайте следить за этим. Объявления типов в функциях помогут вам в любом случае.

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

По вашему предложению

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

Как не использовать нуль

Наконец, есть общие способы использования nullнеправильно. Один из таких способов - использовать его вместо пустых структур данных, таких как массивы и строки. Пустой массив - это правильный массив, как и любой другой! Почти всегда важно и полезно, чтобы структуры данных, которые могут соответствовать нескольким значениям, могли быть пустыми, то есть иметь длину 0.

С точки зрения алгебры пустая строка для строк очень похожа на 0 для чисел, то есть тождество:

a+0=a
concat(str, '')=str

Пустая строка позволяет строкам в целом стать моноидом: https://en.wikipedia.org/wiki/Monoid Если вы его не получите, это не так важно для вас.

Теперь давайте посмотрим, почему это важно для программирования на следующем примере:

for (element in array) {
  doSomething(element);
}

Если мы передадим пустой массив здесь, код будет работать нормально. Это просто ничего не сделает. Однако, если мы передадим nullздесь, то, скорее всего, получим сбой с ошибкой типа «не могу пройти через ноль, извините». Мы могли бы обернуть это, ifно это не так чисто, и опять же, вы можете забыть проверить это

Как обращаться с нулем

Что doSomethingAboutIt()следует делать, и особенно то, должно ли оно генерировать исключение, это еще один сложный вопрос. Короче говоря, это зависит от того, nullбыло ли приемлемое входное значение для данной задачи и что ожидается в ответ. Исключения составляют события, которые не были ожидаемы. Я не буду углубляться в эту тему. Этот ответ очень длинен уже.

Gherman
источник
5
На самом деле, horrible error: wanted value but got null!это намного лучше, чем более типичный Segmentation fault. Core dumped....
Тоби Спейт
2
@TobySpeight Верно, но я бы предпочел что-то, что лежит в терминах пользователя, как Error: the product you want to buy is out of stock.
Герман
Конечно, я просто заметил, что это может быть (и часто бывает) еще хуже . Я полностью согласен с тем, что было бы лучше! И ваш ответ в значительной степени соответствует тому, что я сказал бы на этот вопрос: +1.
Тоби Спейт
2
@Gherman: Передача nullтуда, где nullне разрешено, является ошибкой программирования, то есть ошибкой в ​​коде. Отсутствие товара - это часть обычной бизнес-логики, а не ошибка. Вы не можете и не должны пытаться перевести обнаружение ошибки в обычное сообщение в пользовательском интерфейсе. Немедленное завершение работы и отказ от выполнения большего количества кода является предпочтительным вариантом действий, особенно если это важное приложение (например, приложение, которое выполняет реальные финансовые транзакции).
Кристиан Хакл
1
@EricDuminil Мы хотим устранить (или уменьшить) возможность ошибиться, а не nullполностью удалить себя, даже из фона.
Герман
4

В дополнение ко всем очень хорошим ответам, которые были даны ранее, я бы добавил, что каждый раз, когда у вас возникает желание оставить поле пустым, тщательно подумайте, может ли это быть тип списка. Обнуляемый тип эквивалентен списку из 0 или 1 элементов, и часто его можно обобщить до списка из N элементов. В этом случае, возможно, вы захотите рассмотреть lastChangePasswordTimeсписок passwordChangeTimes.

Joel
источник
Это отличный момент. То же самое часто справедливо для типов опций; опция может рассматриваться как особый случай последовательности.
Кристиан Хакл
2

Задайте себе вопрос: какое поведение требует поле lastChangePasswordTime?

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

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Если у вас есть отдельное требование о том, что вновь создаваемые участники должны обновить свой пароль, я бы добавил отдельное логическое поле с именем passwordShouldBeChanged и установил бы его в значение true при создании. Затем я бы изменил функциональность метода IsPasswordExpired (), чтобы включить проверку для этого поля (и переименовал метод в ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Сделайте ваши намерения явными в коде.

Рик Д
источник
2

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

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

Обратите внимание, что хотя это и требует небольших затрат, теперь вы ВСЕГДА можете рассчитать, сколько времени прошло с момента последней установки пароля.

jmoreno
источник
1

Оба являются «безопасными / нормальными / правильными», если вызывающий проверяет перед использованием. Проблема в том, что произойдет, если звонящий не проверит. Что лучше, какая-то разновидность нулевой ошибки или использование недопустимого значения?

Нет единого правильного ответа. Это зависит от того, что вы беспокоитесь.

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

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

ANone
источник