Краще пояснення для мов без нуля


225

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

У мене є основна ідея прохолоди типів варіантів, але я не володію знаннями або знаннями мови, щоб найкраще це виразити. Що є чудовим поясненням наступного, написаного у спосіб, доступний пересічному програмісту, до якого ми могли би вказати цій людині?

  • Небажаність наявності посилань / покажчиків за замовчуванням може бути нульовою
  • Як працюють типи варіантів, включаючи стратегії для полегшення перевірки нульових випадків, таких як
    • відповідність шаблону та
    • монадичні розуміння
  • Таке альтернативне рішення, як повідомлення про поїдання нуля
  • (інші аспекти, які я пропустив)

11
Якщо ви додасте теги до цього питання для функціонального програмування або F #, ви обов'язково отримаєте фантастичні відповіді.
Стівен Свенсен

Я додав тег функціонального програмування, оскільки тип опції походить зі світу ml. Я б краще не позначив його F # (занадто конкретним). BTW, хто має власні повноваження таксономії, повинен додати теги типу "можливо" або "тип".
Роман А. Тейчер

4
Я підозрюю, що в таких специфічних тегах мало потреби. Теги в основному полягають у тому, щоб люди могли знайти відповідні запитання (наприклад, "питання, про які я багато знаю, і зможу відповісти", і "функціональне програмування" дуже корисне там. Але щось на кшталт "null" або " type-type "набагато менш корисні. Мало хто, ймовірно, стежить за тегом" type-type ", шукаючи питання, на які вони зможуть відповісти.;)
jalf

Не будемо забувати, що однією з основних причин нуля є те, що комп’ютери розвивалися сильно прив’язаними до теорії множин. Null - це одна з найважливіших множин у всій теорії множин. Без нього цілі алгоритми розбивалися б. Наприклад - виконати сортування злиття. Це передбачає розбиття списку вдвічі кілька разів. Що робити, якщо список становить 7 пунктів? Спочатку ви розділите його на 4 і 3. Потім 2, 2, 2 і 1. Потім 1, 1, 1, 1, 1, 1, 1, і .... null! У Null є мета, лише така, яку ви практично не бачите. Це існує більше для теоретичної сфери.
Стівендесу

6
@steven_desu - я не згоден. На мовах, що зводяться до нуля, ви можете мати посилання на порожній список [], а також посилання на нульовий список. Це питання стосується плутанини між ними.
стусміт

Відповіді:


433

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

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

class Door
    private bool isShut
    private bool isLocked

і зрозуміло, як відобразити три мої стани в ці дві булеві змінні. Але це залишає четверте, небажаний стан є: isShut==false && isLocked==true. Оскільки типи, які я вибрав як своє представлення, визнають цей стан, я повинен витратити ментальні зусилля на те, щоб клас ніколи не потрапляв у цей стан (можливо, чітко кодуючи інваріант). На відміну від цього, якби я використовував мову з алгебраїчними типами даних або перевіряв перерахування, що дозволяє мені визначати

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

тоді я міг би визначитись

class Door
    private DoorState state

і турбот більше немає. Система типів забезпечить наявність лише трьох можливих станів для екземпляра, який class Doorмає бути. Це те, в чому типові системи хороші - явно виключаючи цілий клас помилок під час компіляції.

Проблема nullполягає в тому, що кожен тип опор отримує цей додатковий стан у своєму просторі, який зазвичай небажаний. stringЗмінна може бути будь-яка послідовність символів, або це може бути цей божевільний додаткове nullзначення , яке не відображає моєї проблемної області. TriangleОб'єкт має три Pointс, які самі по собі мають Xі Yцінність, але , на жаль Points або Triangleсамі по собі може бути божевільним нульовим значення , що НЕ має сенс для побудови графіків області я працюю в. Etc.

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

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

де stringтут передбачається ненульовий тип. Тоді немає складних інваріантів для встановлення, і немає несподіваних NullReferenceExceptions, коли намагаються обчислити довжину чийого імені. Система типу гарантує, що будь-який код, який має MiddleNameоблік з обліковими записами, за можливістю його існування None, тоді як будь-який код, що має справу з FirstNameможе, безпечно припустити, що там є значення.

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

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

без турбот. На відміну від цього, в мові з нульовими посиланнями на такі типи, як рядок, то припущення

class Person
    private string FirstName
    private string MiddleName
    private string LastName

ви закінчуєте авторські речі на кшталт

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

який вибухає, якщо об'єкт, що надходить, не має інваріанта всього того, що є недійсним, або

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

або можливо

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

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

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

(Зверніть увагу, що навіть у цих простих прикладах є складніше. Навіть якщо цього FirstNameне може бути null, а stringможе представляти ""(порожня рядок), що, ймовірно, також не є ім'ям людини, яке ми маємо намір моделювати. Як такий, навіть із не- незмінні рядки, все ж може бути так, що ми "представляємо безглузді значення". Знову ж, ви можете вирішити боротьбу з цим або через інваріанти та умовний код під час виконання, або використовуючи систему типу (наприклад, щоб мати NonEmptyStringтип). останнє, можливо, є необдуманим ("хороші" типи часто "закриваються" над набором загальних операцій, наприклад NonEmptyString, не закриваються над.SubString(0,0)), але це демонструє більше балів у дизайнерському просторі. Зрештою, у будь-якій системі типів є деякі складності, від яких буде дуже добре позбутися, та інші складності, позбутися яких по суті важче. Ключовим моментом цієї теми є те, що майже в кожній системі типів перехід від "нульових посилань за замовчуванням" на "нерегульованих посилань за замовчуванням" майже завжди є простою зміною, яка робить систему типів значно кращою у складності боротьби та виключаючи певні типи помилок і безглузді стани. Тож досить божевільно, що так багато мов постійно і знову повторюють цю помилку.)


31
Re: імена - Дійсно. І, можливо, ви дбаєте про моделювання дверей, яка висить відкритою, але з висунутим замком замку, запобігаючи зачиненню дверей. У світі багато складності. Ключовим моментом є не додавати більшої складності при здійсненні відображення між "державами світу" та "програмами" у вашому програмному забезпеченні.
Брайан

59
Що, ти ніколи не відчиняв двері?
Джошуа

58
Я не розумію, чому люди опрацьовують семантику певного домену. Брайан представляв недоліки з нульовим значенням стисло та просто, так, він спростив проблемну область у своєму прикладі, сказавши, що всі мають імена та прізвища. На питання відповів "T", Брайан, - якщо ти коли-небудь в бостоні, я зобов'язаний тобі пивом за всю публікацію, яку ти тут робиш!
акафеном

67
@akaphenom: дякую, але зауважте, що не всі люди п'ють пиво (я непитущий). Але я вдячний, що ви просто використовуєте спрощену модель світу для того, щоб висловити подяку, тому я не буду більше дивитись на хибні припущення вашої світової моделі. : P (Стільки складних у реальному світі! :))
Брайан

4
Як не дивно, у цьому світі є 3-х двері! Вони використовуються в деяких готелях як туалетні двері. Кнопка діє як ключ зсередини, який замикає двері зовні. Він автоматично розмикається, як тільки болт засувки рухається.
comonad

65

Приємна річ у типах варіантів не в тому, що вони необов’язкові. Це те, що всі інші типи це не так .

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

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

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

В ідеалі, у багатьох випадках, коли null не має сенсу, його не можна допускати .

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

Однак якщо типи за замовчуванням не зводяться до нуля, то вам не потрібно перевіряти, чи вони недійсні. Ви знаєте, що вони ніколи не можуть бути недійсними, тому що компілятор / перевірка типу виконує це за вас.

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

Як уже згадували інші, наприклад, у C # або Java, null може означати одну з двох речей:

  1. змінна неініціалізована. В ідеалі це не повинно відбуватися ніколи . Змінна не повинна існувати, якщо вона не ініціалізована.
  2. змінна містить деякі "необов'язкові" дані: вона повинна бути в змозі представляти випадок, коли немає даних . Це іноді необхідно. Можливо, ви намагаєтесь знайти об’єкт у списку, і не знаєте заздалегідь, чи є він там. Тоді нам потрібно вміти представляти, що "жодного об'єкта не знайдено".

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


І у другому значенні ми хочемо, щоб компілятор попередив (зупинив?) Нас, якщо ми спробуємо отримати доступ до таких змінних, не перевіряючи спочатку наявність недійсності. Ось чудова стаття про майбутню функцію C # (нарешті!) Блогу "
Шнайдер

44

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

Потім вони продовжують стверджувати, що це було б досить акуратною ідеєю, якщо ви застосовуєте ненульова для всіх значень, що може бути зроблено, якщо ви додасте таке поняття, як Optionабо Maybeпредставляєте типи, які не завжди мають певне значення. Такий підхід застосовує Хаскелл.

Це все добре! Але це не виключає використання явно зведених / ненульових типів для досягнення того ж ефекту. Чому ж тоді Варіант все-таки хороший? В кінці кінців, Scala підтримує NULLABLE значення (це має до, тому він може працювати з Java - бібліотек) , але опор , Optionsа також.

З. Яка користь, крім того, що не можна повністю видалити нулі з мови?

А. Склад

Якщо ви зробите наївний переклад з нульового коду

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

до коду, що знає параметри

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

немає великої різниці! Але це також жахливий спосіб використання Опцій ... Цей підхід набагато чистіший:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

Або навіть:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Коли ви починаєте розбиратися зі списком параметрів, він стає ще кращим. Уявіть, що сам Список peopleє необов'язковим:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

Як це працює?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

Відповідний код з нульовими перевірками (або навіть elvis?: Операторів) був би болісно довгим. Справжньою хитрістю тут є операція flatMap, яка дозволяє вкладене розуміння параметрів та колекцій таким чином, щоб нівельовані значення ніколи не могли досягти.


8
+1, це хороший момент для підкреслення. Один додаток: в Haskell-land, flatMapце називалося б (>>=), тобто оператором "прив'язування" для монад. Правильно, Haskellers настільки любить flatMapping речі, що ми поміщаємо їх у логотип нашої мови.
CA McCann

1
+1 Сподіваємось, вираз Option<T>ніколи не буде ніколи недійсним. На жаль, Скала так і досі пов’язаний з Java :-) (З іншого боку, якщо Scala не грає добре з Java, хто б цим користувався? Oo)

Досить просто: "Список (null) .headOption". Зауважте, що це означає зовсім інше, ніж повернене значення "None"
Кевін Райт

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

Відмінна відповідь із чудовими прикладами!
thSoft

38

Оскільки, здається, людей цього не вистачає: nullнеоднозначно.

Дата народження Аліси - це null. Що це означає?

Дата смерті Боба - це null. Що це означає?

"Разумною" інтерпретацією може бути те, що дата народження Аліси існує, але невідома, тоді як дата смерті Боба не існує (Боб все ще живий). Але чому ми дійшли до різних відповідей?


Ще одна проблема: nullкрайова справа.

  • Є null = null?
  • Є nan = nan?
  • Є inf = inf?
  • Є +0 = -0?
  • Є +0/0 = -0/0?

Відповіді зазвичай "так", "ні", "так", "так", "ні", "так" відповідно. Божевільні "математики" називають NaN "нікчемністю" і кажуть, що вона порівнює рівну собі. SQL розглядає нулі як не рівні ні до чого (тому вони поводяться як NaN). Можна задатися питанням, що відбувається, коли ви намагаєтесь зберегти ± ∞, ± 0 і NaN в той самий стовпець бази даних (є 2 53 NaN, половина з яких "негативні").

Що ще гірше, бази даних відрізняються тим, як вони поводяться з NULL, і більшість з них не є послідовними (див. Огляд в роботі з NULL в SQLite ). Це досить жахливо.


А тепер до обов’язкової історії:

Нещодавно я створив таблицю бази даних (sqlite3) з п'ятьма колонками a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Оскільки це загальна схема, призначена для вирішення загальної проблеми для досить довільних додатків, є два унікальних обмеження:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_aіснує лише для сумісності з існуючим дизайном додатків (почасти тому, що я не придумав кращого рішення), і не використовується в новому додатку. Завдяки тому , як NULL працює в SQL, я можу вставити (1, 2, NULL, 3, t)і (1, 2, NULL, 4, t)та не порушувати перше обмеження унікальності (бо (1, 2, NULL) != (1, 2, NULL)).

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


FWIW, без попереднього виклику невизначеної поведінки, посилання C ++ не можуть "вказувати на" нульові показники, і неможливо побудувати клас з неініціалізованими змінними посилальних членів (якщо буде викинуто виняток, конструкція не вдається).

Sidenote: Іноді вам можуть потрібні взаємовиключні покажчики (тобто лише один з них не може бути NULL), наприклад, в гіпотетичному iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Натомість я змушений робити подібні речі assert((bool)actionSheet + (bool)alertView == 1).


Фактично математики не використовують поняття "NaN", будьте впевнені.
Нолдорін

@Noldorin: Так, але вони використовують термін "невизначена форма".
IJ Kennedy

@IJKennedy: Це інший коледж, який я знаю, дуже дякую. Деякі "NaN" можуть представляти собою невизначений вигляд, але оскільки FPA не робить символічних міркувань, прирівнювати його до невизначеної форми є досить оманливим!
Нолдорін

Що не так assert(actionSheet ^ alertView)? Чи не може ваша мова XOR розгортається?
кіт

16

Небажаність наявності посилань / покажчиків за замовчуванням може бути нульовою.

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

  1. Посилання / вказівник неініціалізовані: проблема тут така сама, як мутаційність взагалі. Для одного це ускладнює аналіз вашого коду.
  2. Змінна, що є null, насправді щось означає: це той випадок, який типи Option фактично формалізуються.

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

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

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

У F #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

Однак у такій мові, як Java, без прямої підтримки типів Option, у нас є щось на кшталт:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

Таке альтернативне рішення, як повідомлення про поїдання нуля

"Повідомлення з'їсти нуль" об'єктивів-C - це не стільки рішення, скільки спроба полегшити головний біль нульової перевірки. В основному, замість того, щоб кидати виняток із виконання часу, коли намагається викликати метод на нульовому об'єкті, вираз замість цього оцінюється на нуль. Призупиняючи невіру, це як би починається кожен метод примірника if (this == null) return null;. Але тоді виникає втрата інформації: ви не знаєте, чи повернув метод null тому, що це дійсне повернене значення, або тому що об'єкт насправді є null. Це дуже схоже на ковтання винятку, і не досягає жодного прогресу у вирішенні проблем із нульовим викладеним раніше.


Це домашня тварина, але c # навряд чи є подібною до мови.
Роман А. Тейчер

4
Я збирався тут на Java, оскільки C #, ймовірно, мав би приємніше рішення ... але я ціную твій погляд, що люди насправді мають на увазі "мову з синтаксисом, натхненним c". Я пішов вперед і замінив "c-like" заяву.
Стівен Свенсен

З linq, правильно. Я думав про c # і цього не помічав.
Роман А. Тейчер

1
Так, з синтаксисом натхненного c, але я думаю, що я також чув про такі імперативні мови програмування, як python / ruby, з дуже маленьким способом c, як синтаксис, який функціональні програмісти називають c-like.
Роман А. Тейчер

11

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

У мовах вищого рівня наявність "null" незручно, оскільки воно справді передає два різних поняття:

  • Кажуть, що щось не визначено .
  • Сказати, що щось необов’язково .

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

Другий випадок є необов'язковим і найкраще надається явно, наприклад, з типом опції .


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

У С ми могли б мати:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

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

У OCaml той самий код виглядатиме так:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Скажімо тепер, що ми хочемо надрукувати імена всіх водіїв разом з їхніми номерами ліцензій на вантажівку.

В:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

В OCaml це було б:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

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

  • Це коротше.
  • Ви отримуєте набагато кращі гарантії, і нульова перевірка взагалі не потрібна.
  • Компілятор переконався, що ви правильно розібралися з можливістю

В той час, як в C, ви просто могли забути нульовий чек і бум ...

Примітка: ці зразки коду там, де вони не зібрані, але сподіваюся, що ви отримали ідеї.


Я ніколи не пробував цього, але en.wikipedia.org/wiki/Cyclone_%28programming_language%29 стверджує, що дозволяють вказати ненульові покажчики на c.
Роман А. Тейчер

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

Я вважаю NULL, що "посилання, яке може ні на що не вказувати" було винайдено для якоїсь мови Алгола (Wikipedia погоджується, див. En.wikipedia.org/wiki/Null_pointer#Null_pointer ). Але, звичайно, ймовірно, що програмісти збірки ініціалізували свої покажчики на недійсну адресу (читайте: Null = 0).

1
@Stephen: Ми, мабуть, мали на увазі те саме. Для мене вони відмовляють або забороняють використовувати неініціалізовані речі саме тому, що немає сенсу обговорювати невизначені речі, оскільки ми не можемо зробити з ними нічого добросовісного чи корисного. Це не було б жодного інтересу.
bltxd

2
як @tc. каже, null не має нічого спільного з збіркою. У зборі типи, як правило, не зводяться нанівець. Значення, завантажене в регістр загального призначення, може бути нульовим, або це може бути якесь ненульове ціле число. Але це ніколи не може бути недійсним. Навіть якщо ви завантажуєте адресу пам'яті в реєстр, у більшості поширених архітектур немає окремого подання "нульового вказівника". Це поняття, запроваджене мовами вищого рівня, як C.
jalf

5

Microsoft Research має інстинктивний проект під назвою

Спец. №

Це розширення C # з ненульовим типом і деякий механізм, щоб перевірити ваші об'єкти на предмет недійсності , хоча IMHO, застосовуючи дизайн за принципом контракту, може бути більш доречним і кориснішим для багатьох проблемних ситуацій, викликаних нульовими посиланнями.


4

Виходячи з .NET фону, я завжди думав, що null має крапку, її корисно. Поки я не дізнався про структури та про те, як легко було з ними працювати, уникаючи великого кодового коду. Тоні Хоар, виступаючи в Лондоні QCon в 2009 році, вибачився за винахід нульової посилання . Цитувати його:

Я називаю це моєю помилкою в мільярд доларів. Це був винахід нульової посилання в 1965 році. У той час я розробляв першу комплексну систему для посилань на об'єктно-орієнтованій мові (ALGOL W). Моя мета полягала в тому, щоб будь-яке використання посилань було абсолютно безпечним, а перевірка виконувалась автоматично компілятором. Але я не міг протистояти спокусі ввести нульову посилання просто тому, що це було так просто здійснити. Це призвело до незліченних помилок, уразливості та збоїв у системі, які, ймовірно, спричинили біль і шкоду в мільярд доларів за останні сорок років. Останніми роками ряд програмних аналізаторів, таких як PREfix та PREfast в Microsoft, використовувались для перевірки посилань та попередження, якщо існує ризик, що вони можуть бути недійсними. Більш недавні мови програмування, як Spec #, ввели декларації для ненульових посилань. Це рішення, яке я відкинув у 1965 році.

Дивіться це питання також у програмістів



1

Я завжди дивився на Null (або на нуль) як на відсутність значення .

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

Я також бачив код перевантажений і занадто складний з перевірками на null. Для мене це означає одну з двох речей:
а) помилку вище в дереві додатків;
б) поганий / неповний дизайн

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

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


Привіт @Jon, трохи важко слід за тобою тут. Нарешті я зрозумів, що під "спеціальними / дивними" значеннями ви, мабуть, маєте на увазі щось на кшталт "невизначеного" Javascript або IEEE "NaN". Але крім цього, ви насправді не вирішуєте жодного із запитань ОП. І твердження про те, що "Null - це, мабуть, найкорисніше поняття для перевірки того, чи щось відсутнє" майже напевно неправильне. Типи варіантів - це добре розглянута, безпечна для альтернативи нуль.
Стівен Свенсен

@Stephen - Насправді, озираючись на моє повідомлення, я думаю, що всю другу половину слід перенести на питання, про яке ще не потрібно ставити запитання. Але я все-таки кажу, що null дуже корисний для перевірки, чи є щось відсутнє.
Джон

0

Векторні мови іноді можуть уникнути, не маючи нуля.

Порожній вектор служить введеним нулем в цьому випадку.


Я думаю, я розумію, про що ви говорите, але чи могли б ви перелічити кілька прикладів? Особливо застосувати кілька функцій до можливо нульового значення?
Роман А. Тейчер

Добре застосувати векторне перетворення до порожнього вектора призводить до іншого порожнього вектора. FYI, SQL - це здебільшого векторна мова.
Джошуа

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