Чи краще нове булеве поле, ніж нульове посилання, коли значення може бути значимо відсутнє?


39

Наприклад, припустимо, у мене є клас Member, у якого є останнійChangePasswordTime:

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 неправдивий, lastChangePasswordTime повинен бути нульовим, а перевірка lastChangePasswordTime == null майже ідентична перевіренню isPasswordChanged false, тому я вважаю за краще перевірити lastChangePasswordTime == null безпосередньо

  2. Змінюючи логіку тут, я можу забути оновити обидва поля.

Примітка: коли користувач змінює паролі, я б записував такий час:

this.lastChangePasswordTime=Date.now();

Тут додаткове булеве поле краще, ніж нульове посилання?


51
Це питання є досить мовним, адже те, що є хорошим рішенням, багато в чому залежить від того, що пропонує ваша мова програмування. Наприклад, у C ++ 17 або Scala ви б використовували std::optionalабо Option. В інших мовах, можливо, вам доведеться самостійно побудувати відповідний механізм, або ви можете насправді вдатися до nullчогось подібного, оскільки це більш ідіоматично.
Крістіан Хакл

16
Чи є причина, чому ви не хочете, щоб останнійChangePasswordTime встановив час створення пароля (врешті-решт, створення є модним)?
Крістіан Н

@ChristianHackl Хм, погоджуйтеся, що існують різні "ідеальні" рішення, але я не бачу жодної (основної) мови, хоча там, де використання окремого булевого тексту було б кращою ідеєю, ніж робити нульові / нульові перевірки. Не зовсім впевнений, що стосується C / C ++, оскільки я там не працював довгий час.
Френк Хопкінс

@FrankHopkins: Прикладом можуть бути мови, де змінні можна залишити неініціалізованими, наприклад, C або C ++. lastChangePasswordTimeможе бути там неініціалізованим вказівником, і порівнювати його з чим-небудь було б невизначеною поведінкою. Не дуже переконлива причина не ініціалізувати вказівник на NULL/ nullptrнатомість, особливо не в сучасному C ++ (де ви б зовсім не використовували вказівник), але хто знає? Іншим прикладом можуть бути мови без покажчиків або, можливо, погана підтримка покажчиків. (FORTRAN 77 приходить на думку ...)
Крістіан Хакл

Я би використав для цього перерахунок з 3 випадками :)
J. Doe

Відповіді:


72

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

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

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

}

На мою думку, це робиться так:

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

Спосіб збереження даних (імовірно, в базі даних) несе відповідальність за збереження нулів.


29
+1 для відкриття. Вантонне використання нуля, як правило, не схвалюється лише у випадках, коли нуль є несподіваним результатом, тобто там, де це не має сенсу (для споживача). Якщо null має значення, то це не несподіваний результат.
Площа

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: просто з історичного інтересу - Die Hoare коли-небудь скаже, яка краща практична альтернатива була б (без переходу на абсолютно нову парадигму)?
AnoE

5
@AnoE: цікаве запитання. Не знаєте, що Хоар мав сказати з цього приводу, але безрегулярне програмування часто є реальністю в наші дні сучасними, добре розробленими мовами програмування.
Крістіан Хакл

10
@Том ват? У таких мовах, як C # та Java, DateTimeполе є вказівником. Вся концепція nullмає сенс лише для покажчиків!
Тодд Сьюелл

16
@Tom: Нульові покажчики небезпечні лише для мов та реалізацій, які дозволяють їх сліпо індексувати і відкидати, з будь-якою швидкістю. У мові, яка захоплює спроби індексувати або відкидати нульовий покажчик, це буде часто менш небезпечно, ніж покажчик на манекеновий об’єкт. Існує багато ситуацій, коли наявність аномально припиненої програми може бути кращою, ніж вона створює безглузді числа, а в системах, які їх захоплюють, нульові покажчики є правильним рецептом цього.
supercat

29

Залежно від вашої мови програмування, можуть бути хороші альтернативи, такі як optionalтип даних. У C ++ (17) це буде std :: необов'язково . Він може бути відсутнім або мати довільне значення базового типу даних.


8
Там Optionalтеж у яві; сенс нулевого Optionalзалежить від вас ...
Матьє М.

1
Сюди приїхали, щоб також з'єднатись із Факультативом. Я б кодував це як тип мовою, яка їх мала.
Zipp

2
@FrankHopkins Прикро, що ніщо на Java не заважає тобі писати Optional<Date> lastPasswordChangeTime = null;, але я б не відмовився саме тому. Натомість я би прищепив дуже твердо проведене командне правило, що жодних необов'язкових значень ніколи не дозволяється призначати null. Необов’язково купує вам занадто багато приємних функцій, щоб легко відмовитися від цього. Ви можете легко призначити значення за замовчуванням ( orElseабо з ледачою оцінкою:) orElseGet, викинути помилки ( orElseThrow), перетворити значення на нульовий безпечний спосіб ( map, flatmap) тощо.
Олександр

5
Перевірка @FrankHopkins isPresent- це майже завжди кодовий запах, на мій досвід, і досить надійний знак того, що чорти, які використовують ці опціони, "не вистачають точки". Дійсно, це в основному не дає ніякої користі ref == null, але це також не те, чим слід часто користуватися. Навіть якщо команда нестабільна, інструмент статичного аналізу може встановити закон, як лінійка або FindBugs , який може зафіксувати більшість випадків.
Олександр

5
@FrankHopkins: Можливо, спільноті Java просто потрібна невелика зміна парадигми, яка може зайняти ще багато років. Наприклад, якщо ви дивитесь на Scala, вона підтримує, nullоскільки мова на 100% сумісна з Java, але ніхто не використовує її, і вона Option[T]є повсюдно, з відмінною підтримкою функціонального програмування, як map, flatMapузгодження шаблонів тощо, що йде далеко, далеко поза == nullчеками. Ви маєте рацію, що теоретично тип варіанту звучить як трохи більше, ніж прославлений покажчик, але реальність доводить цей аргумент неправильно.
Крістіан Хакл

11

Правильно використовуючи null

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

У цих ситуаціях ви здебільшого використовуєте його так (у псевдокоді):

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

doSomethingUseful(value);

Проблема

І це дуже велика проблема. Проблема полягає в тому, що до моменту виклику doSomethingUsefulзначення, можливо, не було перевірено null! Якщо цього не було, програма, швидше за все, вийде з ладу. І користувач може навіть не бачити жодних гарних повідомлень про помилки, залишаючись з чимось на кшталт "жахлива помилка: потрібне значення, але отримане недійсне!" (після оновлення: хоча Segmentation fault. Core dumped.в деяких випадках може бути навіть менш інформативна помилка на кшталт або, що ще гірше, відсутність помилок та неправильних маніпуляцій з null)

Забути писати чеки для 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

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

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

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

Порожній рядок дозволяє рядкам взагалі стати моноїдом: https://en.wikipedia.org/wiki/Monoid Якщо ви цього не отримаєте, це для вас не так важливо.

Тепер давайте розберемося, чому це важливо для програмування на цьому прикладі:

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

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

Як поводитися з нулем

Що doSomethingAboutIt()слід робити, а особливо, чи варто викидати виняток - ще одне складне питання. Коротше кажучи, це залежить від того, чи nullбуло прийнятним вхідне значення для даної задачі і що очікується у відповідь. Винятки становлять події, яких не очікували. Я не буду заглиблюватися в цю тему. Ця відповідь вже давно.


5
Насправді, horrible error: wanted value but got null!це набагато краще, ніж більш типовий Segmentation fault. Core dumped....
Toby Speight

2
@TobySpeight Правда, але я вважаю за краще щось, що лежить в термінах користувача, наприклад Error: the product you want to buy is out of stock.
Герман

Звичайно - я просто зауважував, що це може бути (а часто і є) ще гірше . Я повністю погоджуюся, що було б краще! І ваша відповідь - це те, що я б сказав на це запитання: +1.
Toby Speight

2
@Gherman: Пропуск, nullде nullзаборонено, - це помилка програмування, тобто помилка в коді. Товар, який немає на складі, є частиною звичайної бізнес-логіки, а не помилкою. Ви не можете і не повинні намагатися перевести виявлення помилки на звичайне повідомлення в інтерфейсі користувача. Кращий збіг та негайне виконання коду - це особливо важливий додаток (наприклад, той, який здійснює реальні фінансові операції).
Крістіан Хакл

1
@EricDuminil Ми хочемо видалити (або зменшити) можливість зробити помилку, а не видалити nullсебе повністю, навіть з фону.
Герман

4

На додаток до всіх раніше дуже хороших відповідей, я додам, що кожного разу, коли ви спокушаєтесь дозволити поле бути недійсним, подумайте, чи може це бути тип списку. Нульовий тип - це еквівалентно перелік 0 або 1 елемента, і часто це може бути узагальнено до списку з N елементів. Зокрема, у цьому випадку ви можете розглянути lastChangePasswordTimeсписок passwordChangeTimes.


Це відмінний момент. Те саме часто стосується типів варіантів; варіант може розглядатися як окремий випадок послідовності.
Крістіан Хакл

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 - дата та час встановлення поточного пароля.

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


1

Обидва є "безпечними / безпечними / правильними", якщо абонент перевіряє перед використанням. Питання в тому, що станеться, якщо абонент не перевірить. Що краще, якийсь смак нульової помилки або використання недійсного значення?

Єдиної правильної відповіді немає. Це залежить від того, що ви переживаєте.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.