Чи нульові посилання справді погані?


161

Я чув, як говорилося, що включення нульових посилань у мови програмування є "помилкою в мільярд доларів". Але чому? Звичайно, вони можуть викликати NullReferenceExceptions, але що робити? Будь-який елемент мови може бути джерелом помилок при неправильному використанні.

А яка альтернатива? Я думаю, замість того, щоб сказати це:

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Можна сказати так:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Але як це краще? У будь-якому випадку, якщо ви забудете перевірити наявність клієнта, ви отримаєте виняток.

Я припускаю, що налагодження CustomerNotFoundException трохи простіше, ніж NullReferenceException, завдяки більш описовій. Це все є для цього?


54
Я з обережністю ставлюся до підходу "якщо Клієнт існує / тоді отримайте Замовника", як тільки ви напишете два рядки коду, як це, ви відкриєте двері для перегонів. Отримайте, а потім безпечніше перевірити, чи немає винятків з нуля / лову.
Carson63000

26
Якби тільки ми могли усунути джерело всіх помилок часу виконання, наш код буде гарантовано ідеальним! Якщо посилання NULL у C # розбиває ваш мозок, спробуйте невірні, але не-NULL, покажчики на C;)
LnxPrgr3

хоча в деяких мовах є оператори з’єднання нуля та безпечної навігації
jk.

35
null per se непогано. Система типів, яка не має різниці між типом T і типом T + null, є поганою.
Інго

27
Повернувшись до власного запитання через кілька років, я зараз повністю перетворюю підхід Варіант / Можливо.
Тім Гудман

Відповіді:


91

null - зло

Існує уявлення про InfoQ по цій темі: Null Посилання: мільярд доларів Mistake від Тоні Хоара

Тип опції

Альтернативою функціонального програмування є використання типу Option , який може містити SOME valueабо NONE.

Хороша стаття "Варіант" Шаблон, який обговорює тип Варіант та забезпечує його реалізацію для Java.

Я також знайшов звіт про помилки для Java щодо цієї проблеми: Додайте типи параметрів Nice в Java, щоб запобігти NullPointerExceptions . Запрошення функція була введена в Java 8.


5
Nullable <x> в C # - це аналогічне поняття, але не відповідає реалізації схеми варіантів. Основна причина полягає в тому, що в .NET System.Nullable обмежений типами значень.
MattDavey

27
+1 Для типу Варіант. Ознайомившись із " Можливо" Хаскелла , нулі починають виглядати дивно ...
Андрес Ф.

5
Розробник може помилитися де завгодно, тому замість нульового виключення вказівника ви отримаєте порожній варіант виключення. Наскільки останній кращий за перший?
greenoldman

7
@greenoldman не існує такого поняття, як "виключення порожнього варіанта". Спробуйте вставити значення NULL у стовпці бази даних, визначені як NOT NULL.
Йонас

36
@greenoldman Проблема з нулями полягає в тому, що все може бути недійсним, і як розробник, ви повинні бути особливо обережними. StringТип не є на самому ділі рядком. Це String or Nullтип. Мови, які повністю дотримуються ідеї типів опцій, забороняють нульові значення в їхній системі типів, що дає гарантію, що якщо ви визначите підпис типу, який вимагає String(або що-небудь інше), він повинен мати значення. OptionТипу там , щоб дати уявлення типу рівня речей , які можуть або не можуть мати значення, так що ви знаєте , де ви повинні явно обробляти такі ситуації.
KChaloux

127

Проблема полягає в тому, що оскільки теоретично будь-який об’єкт може бути нульовим і підкидати виняток при спробі його використання, ваш об'єктно-орієнтований код в основному являє собою сукупність невибухових бомб.

Ви маєте рацію, що витончена робота з помилками може бути функціонально ідентичною ifзаявам, що перевіряють нуль . Але що відбувається, коли те, що ви переконали в собі, не може бути нульовим, насправді є нульовим? Кербум. Що б далі не було, я готовий зробити ставку, що 1) це не буде витончено і 2) вам не сподобається.

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


27
+1 Зазвичай помилки у виробничому коді НЕОБХІДНО визначати якомога швидше, щоб їх можна було виправити (як правило, помилка даних). Чим простіше налагоджувати, тим краще.

4
Ця відповідь однакова, якщо застосовано до будь-якого із прикладів ОП. Або ви перевіряєте на наявність помилок, або у вас немає, і ви або витончено обходитесь з ними, або ні. Іншими словами, видалення нульових посилань з мови не змінює класи помилок, які ви зазнаєте, це просто тонко змінить прояв цих помилок.
Даш-Том-Банг

19
+1 за вибухові бомби. З нульовими посиланнями, приказка public Foo doStuff()не означає "робити речі і повертати результат Foo", це означає "робити речі і повертати результат Foo, АБО повертати нуль, але ви не маєте поняття, буде це чи ні". Результат полягає в тому, що вам доведеться переконатись у НІЧОМУ, ЩО БУДЕ бути певним. Можна також видалити нулі і використовувати спеціальний тип або візерунок (наприклад, тип значень) для позначення безглуздого значення.
Метт Оленік

12
@greenoldman, ваш приклад є вдвічі більш ефективним, щоб продемонструвати нашу думку про те, що використання примітивних типів (нульових чи цілих чисел) як семантичних моделей є поганою практикою. Якщо у вас є тип, на який негативні числа не є дійсною відповіддю, вам слід створити новий клас типів, щоб реалізувати семантичну модель із таким значенням. Тепер весь код для моделювання цього негативного типу локалізується на його класі, ізольованому від решти системи, і будь-яка спроба створити для нього негативне значення може бути негайно спіймана.
Хупернікетес

9
@greenoldman, ви продовжуєте доводити те, що примітивні типи неадекватні для моделювання. По-перше, це було понад негативні числа. Тепер це більше оператора ділення, коли нуль - дільник. Якщо ви знаєте, що ви не можете розділити на нуль, то ви можете передбачити, що результат не буде гарним, і ви несете відповідальність за тест на тестування та надаєте відповідне "дійсне" значення для цього випадку. Розробники програмного забезпечення несуть відповідальність за те, щоб знати параметри, в яких працює їх код, і кодувати їх належним чином. Це те, що відрізняє інженерію від майстерності.
Хупернікетес

50

Існує кілька проблем із використанням нульових посилань у коді.

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

По-друге, налагоджувальний код стає складнішим, коли з'являється нульова посилання, і ви намагаєтеся визначити, що його генерувало , який стан діє та його причину, навіть якщо ви можете простежити його шлях виконання вище.

По-третє, нульові посилання вносять додаткові кодові шляхи для тестування .

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

По-п'яте, час виконання мови вже виконує перевірку типу, коли він здійснює пошук селектора в таблиці методів об'єкта. Таким чином, ви дублюєте зусилля, перевіряючи, чи є тип об'єкта дійсним / недійсним, а потім виконувати перевірку типу дійсного об'єкта, щоб викликати його метод.

Чому б не скористатися шаблоном NullObject, щоб скористатися перевіркою виконання, щоб він посилався на методи NOP, характерні для цього стану (відповідно до інтерфейсу звичайного стану), а також усунув усі додаткові перевірки нульових посилань на всій вашій кодовій базі?

Він передбачає більше роботи , створюючи клас NullObject для кожного інтерфейсу, з яким потрібно представляти спеціальний стан. Але принаймні спеціалізація виділяється для кожного спеціального стану, а не від коду, в якому може бути присутнім стан. IOW, кількість тестів зменшується, оскільки у вас менше альтернативних шляхів виконання у ваших методах.


7
... тому, що з'ясувати, хто створив (а точніше, не покладався змінити чи ініціалізувати) дані про сміття, які заповнюють ваш нібито корисний об'єкт, набагато простіше, ніж відслідковувати NULL-посилання? Я його не купую, хоча я в основному застряг у статично набраній землі.
Даш-Том-Банг

Дані про сміття набагато рідше, ніж нульові посилання. Особливо в керованих мовах.
Дін Хардінг

5
-1 для Null Object.
DeadMG

4
Бали (4) і (5) є суцільними, але NullObject не є засобом захисту. Ви потрапите в ще гірші пастки - логічні помилки (робочий процес). Коли ви точно знаєте поточну ситуацію, то, звичайно, це може допомогти, але використання сліпо (замість нуля) обійдеться вам дорожче.
greenoldman

1
@ gnasher729, хоча час роботи Objective-C не викине виключення з нульовим покажчиком, як це роблять Java та інші системи, але це не робить кращого використання нульових посилань з причин, які я виклав у своїй відповіді. Наприклад, якщо в [self.navigationController.presentingViewController dismissViewControllerAnimated: YES completion: nil];процесі виконання немає навігаційного контролера, Контролер розглядає це як послідовність відключення, погляд не видаляється з дисплея, і ви можете сидіти перед Xcode, чухаючи голову годинами, намагаючись з’ясувати, чому.
Хупернікетес

48

Нули не такі вже й погані, якщо ви їх не очікуєте. Ви повинні повинні явно вказати в коді , який ви очікували нульовий, що є проблемою мови дизайну. Врахуйте це:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

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

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


11
Ого, це досить круто. Окрім того, що під час компіляції з'являється набагато більше помилок, це позбавляє мене від необхідності перевіряти документацію, щоб з’ясувати, чи GetByLastNameповертається null, якщо його не знайдено (на відміну від викидання винятку) - я просто можу побачити, повертається Customer?чи Customer.
Тім Гудман

14
@JBRWilkinson: Це лише приготований приклад для показу ідеї, а не приклад фактичної реалізації. Я насправді думаю, що це досить акуратна ідея.
Дін Хардінг

1
Ти маєш рацію, що це не нульовий шаблон об'єкта.
Дін Хардінг

13
Це схоже на те, що Haskell називає a Maybe- A Maybe Customerє або Just c(де ca Customer) або Nothing. В інших мовах це називається Option- дивіться деякі інші відповіді на це питання.
MatrixFrog

2
@SimonBarker: ви можете бути впевнені, що Customer.FirstNameвона має тип String(на відміну від String?). У цьому справжня вигода - лише змінні / властивості, які не мають значущого нічого значення, повинні бути оголошені нульовими.
Йогу

20

Є безліч відмінних відповідей, які висвітлюють нещасні симптоми null, тому я хотів би представити альтернативний аргумент: Null є вадою в системі типу.

Мета системи типу - забезпечити, щоб різні компоненти програми «добре поєднувалися» належним чином; добре набрана програма не може «вийти з рейок» у невизначене поведінку.

Розглянемо гіпотетичний діалект Java або будь-який улюблений у вас "Hello, world!"тип статичної мови, де ви можете призначити рядок будь-якій змінній будь-якого типу:

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

І ви можете перевірити такі змінні:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

У цьому немає нічого неможливого - хтось міг розробити таку мову, якби хотів. Особливе значення не повинно бути "Hello, world!"- це , можливо, було число 42, кортеж (1, 4, 9), або, скажімо, null. Але чому б ти це робив? Змінна типу Fooповинна містити лише Foos - у цьому вся суть системи типів! nullце не Fooбільше, ніж "Hello, world!"є. Гірше - nullце не значення будь-якого типу, і ви нічого з цим не можете зробити!

Програміст ніколи не може бути впевнений, що змінна насправді містить a Foo, а також програма не може; щоб уникнути невизначеної поведінки, вона повинна перевірити змінні, "Hello, world!"перш ніж використовувати їх як Foos. Зауважте, що проведення перевірки рядків у попередньому фрагменті не поширює факту, що foo1 насправді є Foo- barймовірно, буде мати і свою перевірку, просто для безпеки.

Порівняйте це з використанням a Maybe/ Optiontype із узгодженням шаблону:

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

Всередині цього Just fooпункту і ви, і програма точно знаєте, що наша Maybe Fooзмінна дійсно містить Fooзначення - ця інформація поширюється вниз по ланцюгу викликів, і barне потрібно робити ніяких перевірок. Оскільки Maybe Fooце відмінний тип від Fooвас, ви змушені керуватись можливістю, яку він може містити Nothing, тому ви не зможете ніколи закрити сліпою a NullPointerException. Ви можете набагато легше міркувати про свою програму, і компілятор може опустити нульові перевірки, знаючи, що всі змінні типу Fooдійсно містять Foos. Всі перемагають.


Що з нульовими значеннями в Perl 6? Вони досі нульові, за винятком того, що вони завжди набираються. Чи все-таки проблема, яку ви можете написати my Int $variable = Int, де Intнульове значення типу Int?
Конрад Боровський

@xfix Я не знайомий з Perl, але поки у вас немає способу примусового виконання this type only contains integers, на відміну від this type contains integers and this other valueвас, у вас виникнуть проблеми щоразу, коли ви хочете лише цілі числа.
Довал

1
+1 для відповідності шаблону. З кількістю відповідей вище, в яких згадуються типи варіантів, ви можете подумати, що хтось згадав би той факт, що проста функція мови, наприклад, відповідність шаблонів, може зробити їх набагато більш інтуїтивно зрозумілим, ніж нульові.
Жуль

1
Я думаю, що монадичне прив'язування навіть корисніше, ніж відповідність шаблонів (хоча, звичайно, прив'язка для Можливо, реалізується за допомогою відповідності шаблонів). Я думаю , що це свого роду кумедні бачать мови , як C # покласти спеціальний синтаксис для багатьох речей ( ??, ?., і так далі) для того , що мови , як Haskell реалізують , як бібліотечні функції. Я думаю, що це набагато акуратніше. null/ Nothingпоширення жодним чином не є "особливим", тож чому ми мусимо вивчати більше синтаксису, щоб мати його?
сара

14

(Кидаючи капелюх на ринг для старого питання;))

Специфічна проблема з null полягає в тому, що він порушує статичне введення тексту.

Якщо у мене є Thing t, то компілятор може гарантувати, що я можу зателефонувати t.doSomething(). Ну, UNLESS tнедійсний під час виконання. Тепер усі ставки відключені. Компілятор сказав, що це нормально, але я дізнаюся набагато пізніше, що tНЕ насправді doSomething(). Тому замість того, щоб мати можливість довіряти компілятору, щоб вловлювати помилки типу, я повинен зачекати, поки час виконання, щоб їх виловити. Я б також міг просто використовувати Python!

Тож у певному сенсі null вводить динамічне введення в статично типовану систему з очікуваними результатами.

Різниця між тим, що ділиться на нуль або лог від’ємника тощо, полягає в тому, що коли i = 0, це все ще є int. Компілятор все ще може гарантувати його тип. Проблема полягає в тому, що логіка неправильно застосовує це значення способом, який не дозволено логікою ... але якщо логіка робить це, це значною мірою визначення помилки.

(Компілятор може вирішити деякі з цих проблем, BTW. Як і позначення виразів, як i = 1 / 0під час компіляції. Але ви не можете реально розраховувати, що компілятор буде виконувати функцію і гарантувати, що параметри відповідають усім логіці функції)

Практична проблема полягає в тому, що ви робите багато зайвої роботи та додаєте нульові перевірки, щоб захистити себе під час виконання, але що робити, якщо ви її забули? Компілятор не дозволяє призначити:

Рядок s = новий цілий (1234);

То чому він повинен дозволяти присвоювати значення (null), яке порушує де-посилання на s?

Змішуючи в коді "не значення" з "набраними" посиланнями, ви додатково навантажуєте програмістів. А коли NullPointerExceptions трапляються, їх відстеження може зайняти ще більше часу. Замість того , щоб покладатися на статичної типізації сказати «це є посилання на те , як очікується,» ви дозволяєте язик сказати «це цілком може бути посилання на те , як очікується.»


3
У C змінна типу *intабо ідентифікує int, або NULL. Так само і в C ++, змінна типу *Widgetабо ідентифікує a Widget, річ, тип якої походить від Widget, або NULL. Проблема з Java та похідними на зразок C # полягає в тому, що вони використовують місце зберігання Widgetдля позначення *Widget. Рішення полягає не в тому, щоб робити вигляд, що це *Widgetне повинно бути вміщено NULL, а мати правильний Widgetтип місця зберігання.
supercat

14

nullПокажчик являє собою інструмент

не ворог. Ви просто повинні використовувати їх правильно.

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

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

Мораль історії: Не кидайте дітей водою. І не звинувачуйте інструменти в аварії. Людина, яка давно вигадала число нуль, була досить розумною, оскільки в той день можна було назвати «нічого». nullПокажчик не так далеко від цього.


EDIT: Хоча NullObjectмодель здається кращим рішенням, ніж посилання, які можуть бути нульовими, вона створює проблеми самостійно:

  • Довідка, що займає атрибут a NullObject(повинна, згідно з теорією), нічого не робить, коли викликається метод. Таким чином, тонкі помилки можуть бути введені через помилково непризначені посилання, які зараз гарантовано є ненульовими (так!), Але виконують небажані дії: нічого. З NPE - це очевидно, де проблема криється. З тим, NullObjectщо поводиться якось (але неправильно), ми вводимо ризик виявлення помилок (занадто) пізно. Це не випадково схоже на недійсну проблему вказівника і має подібні наслідки: Посилання вказує на щось, що схоже на дійсні дані, але це не так, вводить помилки даних і їх важко відстежити. Якщо чесно, я б у цих випадках віддав перевагу NPE, який не працює негайно,через логічну / помилку даних, яка раптом вдарить мене пізніше по дорозі. Пам’ятайте дослідження IBM про вартість помилок, які залежать від того, на якому етапі вони виявляються?

  • Поняття нічого не робити, коли метод викликається на a NullObject, не втримується, коли виклик властивості або функція викликається, або коли метод повертає значення через out. Скажімо, повернене значення - це intі ми вирішуємо, що "нічого не робити" означає "повернути 0". Але як ми можемо бути впевнені, що 0 є правильним "нічого", яке потрібно (не) повертати? Зрештою, NullObjectне слід нічого робити, але коли його запитують про значення, воно має якось реагувати. Невдача не є варіантом: ми використовуємо NullObjectдля запобігання NPE, і ми точно не торгуватимемо його іншим винятком (не реалізовано, ділимо на нуль, ...), чи не так? То як правильно ви нічого не повертаєте, коли вам щось потрібно повернути?

Я не можу допомогти, але коли хтось намагається застосувати NullObjectшаблон до кожної проблеми, це більше схоже на спробу виправити одну помилку, зробивши іншу. Це, без сумніву, хороше і корисне рішення в деяких випадках, але це, безумовно, не чарівна куля для всіх випадків.


Чи можете ви представити аргумент, чому воно nullмає існувати? Можна подати хвилю майже про будь-яку мовну ваду з "це не проблема, просто не помиляйтесь".
Доваль

На яке значення ви встановили б недійсний покажчик?
JensG

Що ж, ви не встановлюєте їм ніякого значення, тому що ви взагалі не повинні їх допускати. Натомість ви використовуєте змінну іншого типу, можливо, називається, Maybe Tяка може містити або значення Nothing, або Tзначення. Головне, що ви змушені перевірити, чи Maybeдійсно він містить, Tперш ніж вам дозволяти його використовувати, та надати дію, якщо цього не відбувається. А оскільки ви заборонили недійсні вказівники, тепер ніколи не потрібно перевіряти щось, що не є Maybe.
Доваль

1
@JensG у вашому останньому прикладі, чи компілятор змушує вас робити нульову перевірку перед кожним викликом t.Something()? Або це має певний спосіб дізнатися, що ви вже зробили перевірку, і не змусити вас це зробити ще раз? Що робити, якщо ви зробили перевірку, перш ніж перейти tдо цього методу? З типом Option компілятор знає, чи зробили ви перевірку ще чи ні, переглянувши тип t. Якщо це Опція, ви її не перевірили, тоді як якщо це значення, яке не розгорнуте, то у вас є.
Тім Гудман

1
Що повертає нульовий об'єкт, коли його запитують про значення?
JensG

8

Нульові посилання є помилкою, оскільки вони дозволяють нечуттєвий код:

foo = null
foo.bar()

Є альтернативи, якщо ви використовуєте систему типів:

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

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

У Haskell це є з нуля ( Maybe), у C ++ ви можете скористатися, boost::optional<T>але ви все одно можете отримати невизначене поведінку ...


6
-1 за "важелі"!
Марк C

8
Але, безумовно, безліч загальних програм програмування допускають безглуздий код, ні? Ділення на нуль є (мабуть) безглуздим, але ми все ще допускаємо поділ, і сподіваємось, що програміст пам’ятає перевірити, чи є дільник не нульовим (або обробляє виняток).
Тім Гудман

3
Я не впевнений , що ви маєте на увазі під «з нуля», але Maybeне вбудовані в ядро мови, його визначення як раз трапляється бути в стандартній прелюдії: data Maybe t = Nothing | Just t.
fredoverflow

4
@FredOverflow: оскільки прелюдія завантажена за замовчуванням, я вважаю це "з нуля", що Haskell має достатньо дивовижну систему типів, що дозволить визначити ці (та інші) тонкощі загальними правилами мови - це лише бонус :)
Матьє М.

2
@TimGoodman Хоча поділ на нуль не може бути найкращим прикладом для всіх типів числових значень (IEEE 754 визначає результат ділення на нуль), у випадках, коли він би викинув виняток, замість того, щоб значення типу Tрозділили на інші значення типу T, ви обмежите операцію значеннями типу, Tподіленими на значення типу NonZero<T>.
kbolino

8

А яка альтернатива?

Необов'язкові типи та відповідність шаблонів. Оскільки я не знаю C #, ось фрагмент коду вигаданою мовою під назвою Scala # :-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}

6

Проблема з нулями полягає в тому, що мови, які дозволяють їм в значній мірі змушують вас програмувати оборонно проти цього. Для цього потрібно багато зусиль (набагато більше, ніж намагатися використовувати оборонні if-блоки)

  1. об'єкти, від яких ви очікуєте, що вони не будуть нульовими, насправді ніколи не бувають нульовими, і
  2. що ваші оборонні механізми дійсно ефективно справляються з усіма потенційними НПЗ.

Отже, дійсно, нулі в кінцевому рахунку є дорогою річчю.


1
Це може бути корисно, якщо ви включили приклад, коли потрібно боротися з нулями. У моєму, звичайно, надто спрощеному прикладі зусилля приблизно однакові. Я не згоден з вами, я просто знаходжу конкретні (псевдо) приклади коду, що малюють чіткішу картину, ніж загальні твердження.
Тім Гудман

2
Ах, я не пояснив себе чітко. Справа не в тому, що складніше боротися з нулями. Це вкрай важко (якщо не неможливо) гарантувати, що весь доступ до потенційно нульових посилань уже захищений. Тобто, мовою, яка допускає нульові посилання, просто неможливо гарантувати, що фрагмент коду довільного розміру не містить винятків з нульовим вказівником, не без певних великих труднощів та зусиль. Ось що робить нульові посилання дорогою справою. Надзвичайно дорого гарантувати повну відсутність виключень нульових покажчиків.
luis.espinal

4

Питання, якою мірою ваша мова програмування намагається довести правильність вашої програми ще до її запуску. Статично набраною мовою ви доказуєте, що у вас є правильні типи. Перемістившись за замовчуванням ненульових посилань (з необов'язковими нульовими посиланнями), ви можете усунути багато випадків, коли нуль передано, а цього не повинно бути. Питання полягає в тому, чи варто додаткове зусилля в обробці ненульових посилань корисним з точки зору коректності програми.


4

Проблема не настільки нульова, це те, що ви не можете вказати ненульовий тип посилання на багатьох сучасних мовах.

Наприклад, може виглядати ваш код

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    egg.Crack();
    MixingBowl.Mix(egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

Коли посилання класів будуть передані, захисне програмування рекомендує ще раз перевірити всі параметри на нуль, навіть якщо ви заздалегідь перевірили їх на нуль, особливо під час створення тестових одиниць багаторазового використання. Таку саму посилання можна легко перевірити на нуль 10 разів у кодовій базі!

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

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

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

Цей список також вказаний як # 2, найбільш голосова функція для C # -> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c


Я думаю, що ще одним важливим моментом щодо Варіанту <T> є те, що це монада (а отже, і ендофунктор), тому ви можете складати складні обчислення над потоками значень, що містять необов'язкові типи, не чітко перевіряючи Noneкожен крок шляху.
сара

3

Нуль - це зло. Однак відсутність нуля може бути більшим злом.

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

Що робить пошук Гудмена, коли її там немає? Без нуля вам потрібно повернути якогось замовника за замовчуванням - і тепер ви продаєте речі до цього замовчування.

По суті, це зводиться до того, чи важливіше, щоб код працював незалежно від того, чи він працює правильно. Якщо неправильна поведінка є кращою перед будь-якою поведінкою, тоді ви не хочете нулів. (Для прикладу того, як це може бути правильним вибором, розглянемо перший запуск Ariane V. Помилка uncaught / 0 призвела до того, що ракета зробила важкий поворот, і самознищення вистрілило, коли вона розпалася через це. Значення він намагався обчислити, що насправді більше не виконував жодної мети, як тільки підсилювач прискорився - він вийшов би на орбіту, незважаючи на звичайне повернення сміття.)

Принаймні 99 разів із 100 я вирішив би використовувати нулі. Хоча я стрибну від радості на замітку до самої їх версії.


1
Це хороша відповідь. Нульова конструкція в програмуванні не є проблемою, це всі екземпляри нульових значень. Якщо ви зробите об'єкт за замовчуванням, врешті-решт всі ваші нулі будуть замінені тими за замовчуванням, а ваш інтерфейс користувача, БД і всюди між ними заповниться тими, які, мабуть, більше не корисні. Але ти спатимеш вночі, бо принаймні твоя програма не виходить з ладу.
Кріс МакКолл

2
Ви припускаєте, що nullце єдиний (або навіть кращий) спосіб моделювання відсутності значення.
сара

1

Оптимізуйте для найбільш поширеного випадку.

Потрібно весь час перевіряти наявність нуля - нудно - ви хочете мати можливість просто захопити об'єкт Замовника та працювати з ним.

У звичайному випадку це має спрацювати чудово - ви зробите пошук, дістаньте об'єкт і використовуєте його.

У винятковому випадку , коли ви (випадковим чином) шукаєте клієнта по імені, не знаючи, чи існує цей запис / об'єкт, вам знадобиться певна ознака того, що це не вдалося. У цій ситуації відповідь полягає у тому, щоб кинути виняток RecordNotFound (або дозволити провайдеру SQL нижче зробити це за вас).

Якщо у вас є ситуація, коли ви не знаєте, чи можете ви довіряти вхідним даним (параметру), можливо, тому, що вони були введені користувачем, ви також можете надати "TryGetCustomer (ім'я, клієнт)". візерунок. Перехресне посилання з int.Parse та int.TryParse.


Я б запевнив, що ви ніколи не знаєте, чи можете ви довіряти тим, хто надходить даними, оскільки він надходить до функції і є нульовим типом. Код може бути відновлений, щоб зробити внесок зовсім іншим. Отже, якщо є нульова можливість, ви завжди повинні перевірити. Таким чином, ви закінчуєте систему, де кожна змінна перевіряється на нуль кожен раз, перш ніж вона отримує доступ. Випадки, коли це не перевірено, стають відсотками відмов для вашої програми.
Кріс МакКолл

0

Винятки не є рівними!

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

Уникаючи використання нульових об'єктів, ви приховуєте частину цих винятків. Я не кажу про приклад OP, коли він конвертує нульовий виняток вказівника в добре набраний виняток, це може бути насправді хорошою справою, оскільки збільшує читабельність. Однак коли ви приймаєте рішення типу Option, як вказував @Jonas, ви приховуєте винятки.

Тоді, ніж у вашій виробничій програмі, замість виключення, яке викидаєте при натисканні на кнопку, щоб вибрати порожній варіант, нічого просто не відбувається. Замість нульового вказівника буде викинуто виняток, і ми, мабуть, отримаємо звіт про це виняток (як і у багатьох виробничих додатках у нас такий механізм), і ніж ми могли насправді це виправити.

Зробити свій код бронезахисним, уникаючи винятку - це погана ідея, зробивши вас кодовим захистом, встановивши виняток, ну це шлях, який я обрав би.


1
Я знаю трохи більше про тип Варіант зараз, ніж коли я розміщував питання кілька років тому, оскільки зараз я їх регулярно використовую в Scala. Сенс Варіанту полягає в тому, що він присвоює Noneщось, що не є Варіантом, помилкою під час компіляції, а також використовує, Optionяк якщо б воно має значення, не попередньо перевіряючи, що це помилка часу компіляції. Помилки компіляції, безумовно, приємніші за винятки під час виконання.
Тім Гудман

Наприклад: Ви не можете сказати s: String = None, ви повинні сказати s: Option[String] = None. І якщо sце варіант, ви не можете сказати s.length, проте ви можете перевірити його значення і одночасно витягнути значення, відповідно до шаблону:s match { case Some(str) => str.length; case None => throw RuntimeException("Where's my value?") }
Tim Goodman

На жаль, у Scala ви все ще можете сказати s: String = null, але це ціна сумісності з Java. Зазвичай ми забороняємо подібні речі в нашому коді Scala.
Тім Гудман

2
Однак я погоджуюсь із загальним зауваженням, що винятки (використовуються належним чином) - це не погана річ, а "виправлення" винятку шляхом проковтування - це загалом жахлива ідея. Але для типу Option мета - перетворити винятки на помилки компіляції, що є кращою справою.
Тім Гудман

@TimGoodman - це лише масштабна тактика, чи її можна використовувати в інших мовах, таких як Java та ActionScript 3? Також було б непогано, якби ви могли редагувати свою відповідь з повним прикладом.
Ілля Газман

0

Nullsє проблематичними, оскільки їх потрібно чітко перевірити, але компілятор не може попередити вас, що ви забули їх перевірити. Тільки трудомісткий статичний аналіз може вам це сказати. На щастя, є кілька хороших альтернатив.

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

Використовуйте нульовий шаблон об'єкта. Іноді відсутнє значення є дійсним станом для вашої системи. Нульовий шаблон об'єкта є хорошим рішенням тут. Це дозволяє уникнути необхідності постійних явних перевірок, але все ж дозволяє представляти нульовий стан. Як це стверджують деякі люди, це не "обчислення помилки", оскільки в цьому випадку нульовий стан є дійсним семантичним станом. Незважаючи на це, ви не повинні використовувати цю модель, коли нульовий стан не є дійсним семантичним станом. Вам слід просто вийняти зміну поза рамками.

Використовуйте можливо / варіант. Перш за все, це дає змогу компілятору попередити, що вам потрібно перевірити відсутність значення, але це більше, ніж замінити один тип явної перевірки на інший. Використовуючи Options, ви можете ланцюжок свого коду і продовжувати так, ніби ваше значення існує, не потрібно насправді перевіряти до абсолютної останньої хвилини. У Scala ваш прикладний код виглядатиме приблизно так:

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

У другому рядку, де ми будуємо дивовижне повідомлення, ми не робимо чіткої перевірки, чи був знайдений клієнт, і краса в цьому нам не потрібна . Це не останній рядок, який може бути багато рядків пізніше, або навіть в іншому модулі, де ми явно стосуємося себе, що станеться, якщо Optionє None. Мало того, що якби ми забули це зробити, це не перевірило б тип. Вже тоді це робиться дуже природним чином, без явного ifтвердження.

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


0

Основна проблема NULL полягає в тому, що вона робить систему ненадійною. У 1980 році Тоні Хоаре в роботі, присвяченій нагороді Тюрінга, написав:

І так, найкращі мої поради авторцям та дизайнерам ADA були проігноровані. …. Не дозволяйте цій мові в теперішньому стані використовуватись у додатках, де надійність є критичною, наприклад, атомних електростанцій, крилатих ракет, систем раннього попередження, систем протиракетної оборони. Наступна ракета, яка зійшла з ладу внаслідок помилки мови програмування, може не стати дослідницькою космічною ракетою під час нешкідливого подорожі до Венери: Це може бути ядерна боєголова, яка вибухає над одним із наших власних міст. Надійна мова програмування, що генерує ненадійні програми, становить набагато більший ризик для нашого середовища та для нашого суспільства, ніж небезпечні машини, токсичні пестициди або аварії на атомних електростанціях. Будьте пильні, щоб зменшити ризик, а не збільшувати його.

Мова ADA з цього часу сильно змінилася, однак такі проблеми все ще існують у Java, C # та багатьох інших популярних мовах.

Обов'язком розробника є створення контрактів між клієнтом та постачальником. Наприклад, в C #, як і в Java, ви можете використовувати Genericsдля мінімізації впливу Nullпосилань, створюючи лише для читання NullableClass<T>(два варіанти):

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

а потім використовувати його як

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

або використовувати два варіанти стилю за допомогою методів розширення C #:

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

Різниця значна. Як клієнт методу, ви знаєте, чого очікувати. Команда може мати правило:

Якщо клас не типу NullableClass, то його екземпляр повинен бути недійсним .

Команда може посилити цю ідею, використовуючи Design by Contract та статичну перевірку під час компіляції, наприклад, з попередньою умовою:

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

або для струни

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

Цей підхід може різко підвищити надійність програми та якість програмного забезпечення. «Дизайн за контрактом» - це справа логіки Хоара , яку Бертран Мейєр заповнив у своїй відомій книзі «Об’єктно-орієнтована побудова програмного забезпечення» та Ейфелевій мові в 1988 році, але вона не використовується неправомірно в сучасній розробці програмного забезпечення.


0

Немає.

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

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


-1

По суті, основна ідея полягає в тому, що проблеми з посиланням на нульові покажчики повинні потрапляти під час компіляції замість часу виконання. Отже, якщо ви пишете функцію, яка займає якийсь об'єкт, і ви не хочете, щоб хтось називав її нульовими посиланнями, тоді система типу повинна дозволяти вам вказати цю вимогу, щоб компілятор міг її використовувати, щоб гарантувати, що виклик вашої функції ніколи не буде компілювати, якщо абонент не відповідає вашим вимогам. Це можна зробити багатьма способами, наприклад, прикрашаючи типи або використовуючи типи опцій (які також називаються змінними типами на деяких мовах), але очевидно, що це набагато більше роботи для дизайнерів-компіляторів.

Я думаю, що зрозуміло, чому в 1960–70-х роках дизайнер-компілятор не вдавався все-таки реалізовувати цю ідею, оскільки машини були обмежені в ресурсах, компілятори потрібно було тримати порівняно просто, і ніхто насправді не знав, наскільки це погано вийде. років вниз по лінії.


-1

У мене є одне просте заперечення проти null:

Це повністю порушує семантику вашого коду, вносячи неоднозначність .

Часто ви очікуєте, що результат буде певним. Це семантично звучить. Скажімо, ви попросили базу даних для користувача з певним id, ви очікуєте, що певна версія буде певного типу (= user). Але що робити, якщо користувача цього ідентифікатора немає? Одне (погане) рішення: додавання nullзначення. Тому результат неоднозначний : або він є, userабо є null. Але nullце не очікуваний тип. І ось тут починається кодовий запах .

Уникаючи null, ви робите свій код семантично зрозумілим.

Є завжди способи навколо null: рефакторінга колекціям, Null-objects, і optionalsт.д.


-2

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

  1. Майте місцями за замовчуванням нульовий вказівник, але не дозволяйте коду переглядати їх (або навіть визначати, що вони є нульовими), без збоїв. Можливо, надайте явний спосіб встановлення покажчика на нуль.
  2. Зазначте розташування за замовчуванням на нульовий покажчик та збийте, якщо буде зроблена спроба прочитати його, але надайте засоби без збоїв для тестування, чи покажчик містить нуль. Надайте також експлікаційний засіб встановлення покажчика на нуль.
  3. Зазначте розташування за замовчуванням на вказівник на якийсь конкретний екземпляр за замовчуванням, що постачається компілятором, зазначеного типу.
  4. Зазначте розташування за замовчуванням на вказівник на якийсь конкретний екземпляр за замовчуванням, що постачається програмою, зазначеного типу.
  5. Будь-який доступ з нульовим покажчиком (а не лише операції з перенаправлення) викликає деяку програмну програму для подання екземпляра.
  6. Створіть мовну семантику таким чином, що колекції місць зберігання не існуватимуть, за винятком неприступного компілятора тимчасово, до тих пір, поки підпрограми ініціалізації не будуть запущені для всіх членів (щось на зразок конструктора масиву повинно бути поставлено або з екземпляром за замовчуванням, функція, яка повертає екземпляр або, можливо, пару функцій - одну для повернення екземпляра та ту, яка буде викликана в раніше побудованих екземплярах, якщо виняток виникає під час побудови масиву).

Останній вибір може мати певну привабливість, особливо, якщо мова включає в себе як зведені, так і нерегульовані типи (можна було б викликати спеціальні конструктори масивів для будь-яких типів, але потрібно лише викликати їх під час створення масивів ненульованих типів), але, мабуть, це було б нездійсненно впродовж часу, коли були винайдені нульові покажчики. З інших варіантів жоден не здається привабливішим, ніж дозволяти скопіювати нульові вказівники, але не перенаписати чи індексувати. Підхід №4 може бути зручним, як варіант, і повинен бути досить дешевим у здійсненні, але це, звичайно, не повинно бути єдиним варіантом. Вимагати, щоб покажчики повинні за замовчуванням вказувати на якийсь конкретний дійсний об'єкт, набагато гірше, ніж мати покажчики за замовчуванням на нульове значення, яке можна прочитати чи скопіювати, але не перенаписати чи індексувати.


-2

Так, NULL - це жахливий дизайн , в об'єктно-орієнтованому світі. Коротше кажучи, використання NULL призводить до:

  • спеціальна обробка помилок (замість винятків)
  • неоднозначне смислове
  • повільний, а не швидкий збій
  • комп'ютерне мислення проти об’єктного мислення
  • змінні та неповні предмети

Перегляньте цю публікацію в блозі для детального пояснення: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

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