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

29

Я читал о (не) удобстве иметь nullвместо (например) Maybe. Прочитав эту статью , я убедился, что было бы намного лучше использоватьMaybe (или что-то подобное). Однако я удивлен, увидев, что все «общеизвестные» императивные или объектно-ориентированные языки программирования по-прежнему используют null(что обеспечивает неконтролируемый доступ к типам, которые могут представлять значение «ничего»), и это Maybeв основном используется в функциональных языках программирования.

В качестве примера рассмотрим следующий код C #:

void doSomething(string username)
{
    // Check that username is not null
    // Do something
}

Здесь что-то плохо пахнет ... Почему мы должны проверять, является ли аргумент нулевым? Разве мы не должны предполагать, что каждая переменная содержит ссылку на объект? Как видите, проблема в том, что по определению почти все переменные могут содержать нулевую ссылку. Что если бы мы могли решить, какие переменные являются «обнуляемыми», а какие нет? Это сэкономило бы нам много усилий при отладке и поиске «NullReferenceException». Представьте, что по умолчанию никакие типы не могут содержать нулевую ссылку . Вместо этого вы бы прямо заявили, что переменная может содержать нулевую ссылку , только если она вам действительно нужна. Это идея может быть. Если у вас есть функция, которая в некоторых случаях дает сбой (например, деление на ноль), вы можете вернутьMaybe<int>, явно указав, что результатом может быть int, но тоже ничего! Это одна из причин, почему предпочтение может быть вместо нуля. Если вас интересуют другие примеры, тогда я предлагаю прочитать эту статью .

Факты таковы, что, несмотря на недостатки, заключающиеся в том, что большинство типов по умолчанию обнуляются, большинство языков программирования ОО фактически делают это. Вот почему я задаюсь вопросом о:

  • Какие аргументы вы бы использовали nullвместо своего языка программирования Maybe? Есть ли причины вообще или это просто «исторический багаж»?

Пожалуйста, убедитесь, что вы понимаете разницу между null и Maybe, прежде чем ответить на этот вопрос.

aochagavia
источник
3
Я предлагаю почитать о Тони Хоаре , в частности о его ошибке в миллиард долларов.
Одед
2
Ну, это была его причина. И неудачным результатом является то, что эта ошибка была скопирована на большинство последующих языков вплоть до сегодняшнего дня. Там являются языки , где nullи его концепция не существует (IIRC Haskell является одним из таких примеров).
Одед
9
Исторический багаж не стоит недооценивать. И помните, что операционные системы, на которых они основаны, были написаны на языках, которые были nullв них долгое время. Нелегко просто бросить это.
Одед
3
Я думаю, вы должны пролить некоторый свет на концепцию «Может быть» вместо того, чтобы публиковать ссылку.
Джефф
1
@ GlenH7 Строки в C # являются ссылочными значениями (они могут быть нулевыми). Я знаю, что int - это примитивное значение, но было полезно показать использование может быть.
Aochagavia

Ответы:

15

Я считаю, что это в первую очередь исторический багаж.

Самый выдающийся и самый старый язык с nullC и C ++. Но здесь nullимеет смысл. Указатели по-прежнему довольно числовые и низкоуровневые концепции. И, как сказал кто-то другой, в мышлении программистов на C и C ++ необходимость явно указывать, что указатель может быть null, не имеет смысла.

Второй в очереди идет Java. Учитывая, что разработчики Java пытались приблизиться к C ++, чтобы они могли упростить переход с C ++ на Java, они, вероятно, не хотели возиться с такой основной концепцией языка. Кроме того, для реализации явного выполнения nullпотребуется гораздо больше усилий, поскольку необходимо проверить, правильно ли установлена ​​ненулевая ссылка после инициализации.

Все остальные языки такие же, как Java. Они обычно копируют то, как это делают C ++ или Java, и, учитывая, какова базовая концепция неявных nullссылочных типов, становится действительно трудно разработать язык, использующий явный null.

Euphoric
источник
«Они, вероятно, не хотели связываться с такой основной концепцией языка». Они уже полностью удалили указатели, и null, думаю , удаление не было бы таким большим изменением.
svick
2
@svick Для Java ссылки являются заменой указателей. И во многих случаях указатели в C ++ использовались так же, как ссылки Java. Некоторые люди даже утверждают, что у Java есть указатели ( programmers.stackexchange.com/questions/207196/… )
Euphoric
Я отметил это как правильный ответ. Я хотел бы заявить об этом, но у меня недостаточно репутации.
Aochagavia
1
Согласен. Обратите внимание, что в C ++ (в отличие от Java и C) исключение NULL является исключением. std::stringне может быть null. int&не может быть null. int*Может, и C ++ позволяет Неконтролируемый доступ к ней по двум причинам: 1. потому что C сделал и 2. потому что вы должны понимать , что вы делаете при использовании сырых указателей в C ++.
MSalters
@MSalters: если тип не имеет значения по умолчанию для копирования в битах, то создание массива этого типа потребует вызова конструктора для каждого его элемента, прежде чем разрешить доступ к самому массиву. Это может потребовать бесполезной работы (если некоторые или все элементы будут перезаписаны до того, как они будут прочитаны), может привести к сложностям, если конструктор для более позднего элемента завершится неудачно после того, как был построен более ранний элемент, и может в конечном итоге ничего не достигнуть (если правильное значение для некоторых элементов массива не может быть определено без чтения других).
суперкат
15

На самом деле, nullэто отличная идея. Учитывая указатель, мы хотим указать, что этот указатель не ссылается на допустимое значение. Поэтому мы берем одну ячейку памяти, объявляем ее недействительной и придерживаемся этого соглашения (соглашение, которое иногда применяется с segfaults). Теперь, когда у меня есть указатель, я могу проверить, содержит ли он Nothing( ptr == null) или Some(value)( ptr != null, value = *ptr). Я хочу, чтобы вы поняли, что это эквивалентно Maybeтипу.

Проблемы с этим:

  1. Во многих языках система типов здесь не помогает гарантировать ненулевую ссылку.

    Это исторический багаж, так как многие основные императивные языки или языки ООП имеют прогрессивные улучшения в своих системах типов только по сравнению с предшественниками. Небольшие изменения имеют то преимущество, что новые языки легче изучать. C # является основным языком, который представил инструменты уровня языка для лучшей обработки нулей.

  2. Разработчики API могут возвращаться nullв случае неудачи, но не в случае ссылки на саму вещь в случае успеха. Часто вещь (без ссылки) возвращается напрямую. Такое выравнивание одного уровня указателя делает невозможным использование nullв качестве значения.

    Это просто лень на стороне дизайнера, и с ней нельзя справиться, не навязывая правильное вложение с правильной системой типов. Некоторые люди могут также попытаться оправдать это с соображениями производительности, или с наличием дополнительных проверок (коллекция может вернуть nullили сам элемент, но и обеспечить containsметод).

  3. В Haskell есть аккуратный взгляд на Maybeтип как монаду. Это облегчает составление преобразований для содержащегося значения.

    С другой стороны, языки низкого уровня, такие как C, почти не рассматривают массивы как отдельный тип, поэтому я не уверен, чего мы ожидаем. В языках ООП с параметризованным полиморфизмом Maybeтип, проверяемый во время выполнения, довольно прост для реализации.

Амон
источник
«C # отходит от нулевых ссылок». Какие это шаги? Я не видел ничего подобного в изменениях языка. Или вы имеете в виду, что обычные библиотеки используют nullменьше, чем в прошлом?
svick
@svick Плохая формулировка с моей стороны. « C # является основным языком, который представил инструменты уровня языка для лучшей обработки nulls » - я говорю о Nullableтипах и ??операторе по умолчанию. Это не решает проблему прямо сейчас в присутствии унаследованного кода, но это является шагом на пути к лучшему будущему.
Амон
Я согласен с тобой. Я бы проголосовал за ваш ответ, но у меня не было достаточно репутации :( Однако Nullable работает только для примитивных типов. Так что это всего лишь маленький шаг.
aochagavia
1
@svick Nullable не имеет к этому никакого отношения. Мы говорим обо всех ссылочных типах, неявно допускающих нулевое значение, вместо того, чтобы программист определял его явно. И Nullable может использоваться только для типов значений.
Эйфорическая
@Euphoric Я думаю, что ваш комментарий был задуман как ответ amon, я не упомянул Nullable.
svick
9

Насколько я понимаю, это nullбыла необходимая конструкция для абстрагирования языков программирования от сборки. 1 Программистам нужна была возможность указать, что указатель или значение регистра было not a valid valueи nullстало общим термином для этого значения.

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

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

Прежде чем вы сможете принять решение по любому из этих двух пунктов, вам необходимо определить, что значение maybeбудет означать для вашей системы. Вы можете обнаружить, что это просто переименование nullсмысла, not a valid valueили вы можете обнаружить, что оно имеет другую семантику для вашего языка.

Аналогичным образом, решение о том, проверять ли доступ по nullдоступу или по ссылке, является еще одним дизайнерским решением на вашем языке.

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

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

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

Итак, чтобы ответить на ваши вопросы:

  1. «Что за аргументы…» - Ну, это зависит от того, что вы хотите, чтобы язык делал. Если вы хотите смоделировать доступ к голому металлу, вы можете разрешить это.

  2. "Это просто исторический багаж?" Возможно, возможно нет. nullбезусловно, имеет / имеет значение для ряда языков и помогает управлять выражением этих языков. Исторический прецедент мог повлиять на более поздние языки и их разрешение, nullно немного взмахнуть руками и объявить концепцию бесполезным историческим багажом.


1 См. Эту статью в Википедии, хотя Хоэру отдают должное нулевые значения и объектно-ориентированные языки. Я считаю, что императивные языки развивались по другому генеалогическому древу, чем Алгол.


источник
Дело в том, что большинству переменных, скажем, в C # или Java можно присвоить нулевую ссылку. Похоже, что было бы намного лучше назначать нулевые ссылки только тем объектам, которые явно указывают, что «Возможно» нет ссылки. Итак, мой вопрос о понятии «ноль», а не о слове.
aochagavia
2
«Ссылки на нулевые указатели могут отображаться как ошибки во время компиляции» Какие компиляторы могут это сделать?
svick
Чтобы быть абсолютно справедливым, вы никогда не назначаете пустую ссылку на объект ... ссылка на объект просто не существует (ссылка указывает на ничто (0x000000000), что по определению null).
mgw854
Цитата спецификации C99 говорит о нулевом символе , а не нулевом указателе , это два совершенно разных понятия.
svick
2
@ GlenH7 Это не для меня. Код object o = null; o.ToString();для меня просто компилируется, без ошибок и предупреждений в VS2012. ReSharper жалуется на это, но это не компилятор.
svick
7

Если вы посмотрите на примеры в статье, которую вы цитировали, большая часть использования Maybeне сокращает код. Это не устраняет необходимость проверки Nothing. Разница лишь в том, что это напоминает вам сделать это через систему типов.

Обратите внимание, я говорю «напомнить», а не силой. Программисты ленивы. Если программист убежден, что значение не может быть Nothing, он будет разыменовывать Maybeбез проверки, точно так же, как теперь разыменовывает нулевой указатель. Конечным результатом является преобразование исключения с нулевым указателем в исключение «разыменованный пустой возможно».

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

То , что делает Maybeэто хорошо, когда много решений сделаны с помощью сопоставления с образцом и полиморфизм вместо явных проверок. Например, вы можете создать отдельные функции processData(Some<T>)и processData(Nothing<T>), что вы не можете сделать с null. Вы автоматически перемещаете обработку ошибок в отдельную функцию, что очень желательно в функциональном программировании, где функции передаются и оцениваются лениво, а не всегда вызываются сверху вниз. В ООП предпочтительным способом отсоединения кода обработки ошибок являются исключения.

Карл Билефельдт
источник
Как вы думаете, это будет желательной функцией в новом языке ОО?
Aochagavia
Вы можете реализовать это самостоятельно, если хотите получить преимущества полиморфизма. Единственная вещь, для которой вам нужна языковая поддержка - это не обнуляемость. Я хотел бы увидеть способ указать это для себя, аналогично const, но сделать его необязательным. Некоторые виды низкоуровневого кода, такие как связанные списки, например, было бы очень раздражающим для реализации с объектами, не допускающими значения NULL.
Карл Билефельдт
2
Вы не должны сверяться с типом Maybe. Тип Maybe является монадой, поэтому он должен иметь функции map :: Maybe a -> (a -> b) и bind :: Maybe a -> (a -> Maybe b)определенные в нем значения, чтобы вы могли продолжить и выполнять дальнейшие вычисления по большей части с повторным преобразованием с использованием оператора if else. И getValueOrDefault :: Maybe a -> (() -> a) -> aпозволяет обрабатывать обнуляемый случай. Это намного элегантнее, чем сопоставление с образцом в Maybe aявном виде.
DetriusXii
1

MaybeЭто очень функциональный способ мышления о проблеме - есть вещь, и она может иметь или не иметь определенное значение. Однако в объектно-ориентированном смысле мы заменяем эту идею вещи (независимо от того, имеет ли она ценность или нет) на объект. Понятно, что объект имеет значение. Если это не так, мы говорим, что объект есть null, но на самом деле мы имеем в виду, что объекта вообще нет. Ссылка у нас на объект указывает на ничто. Перевод Maybeв концепцию ОО не делает ничего нового - на самом деле, он просто делает код более загроможденным. Вы все еще должны иметь нулевую ссылку для значенияMaybe<T>, Вы по-прежнему должны делать нулевые проверки (на самом деле, вы должны делать намного больше нулевых проверок, загромождая ваш код), даже если они теперь называются «возможно проверки». Конечно, вы напишите более надежный код, как утверждает автор, но я бы сказал, что это только потому, что вы сделали язык намного более абстрактным и тупым, требующим уровня работы, который в большинстве случаев не нужен. Я бы охотно использовал исключение NullReferenceException время от времени, чем имел дело со спагетти-кодом, выполняющим проверку Maybe каждый раз, когда я получаю доступ к новой переменной.

mgw854
источник
2
Я думаю, что это приведет к меньшему количеству пустых проверок, потому что вам нужно только проверить, видите ли вы Maybe <T> и не нужно беспокоиться об остальных типах.
aochagavia
1
@svick Maybe<T>должен разрешить nullв качестве значения, потому что значение может не существовать. Если у меня есть Maybe<MyClass>, и у него нет значения, поле значения должно содержать нулевую ссылку. Для этого нет ничего, что было бы достоверно безопасно.
mgw854
1
@ mgw854 Конечно, есть. В ОО-языке Maybeможет быть абстрактный класс с двумя классами, которые наследуются от него: Some(у которого есть поле для значения) и None(у которого нет этого поля). Таким образом, значение никогда не бывает null.
svick
6
С "not-nullability" по умолчанию и Maybe <T> вы можете быть уверены, что некоторые переменные всегда содержат объекты. А именно, все переменные, которые не являются Возможно <T>
aochagavia
3
@ mgw854 Смысл этого изменения заключается в увеличении выразительной силы для разработчика. В данный момент разработчик всегда должен предполагать, что ссылка или указатель могут быть нулевыми, и ему необходимо выполнить проверку, чтобы убедиться в наличии полезного значения. Внедрив это изменение, вы даете разработчику возможность заявить, что ему действительно нужно допустимое значение, и проверить компилятор, чтобы убедиться, что оно было передано. Но все же предоставляя разработчикам возможность подписываться и не передавать ничего.
Эйфорическая
1

Концепция nullлегко может быть прослежена до C, но проблема не в этом.

Мой повседневный язык - C #, и я бы придерживался nullодного отличия. C # имеет два вида типов, значений и ссылок . Значения никогда не могут быть null, но бывают моменты, когда я хотел бы выразить мнение, что ни одно значение не является идеальным. Чтобы сделать это, C # использует Nullableтипы, поэтому intбудет значение и int?значение, допускающее значение NULL. Вот как я думаю, что ссылочные типы также должны работать.

Также смотрите: Нулевая ссылка не может быть ошибкой :

Пустые ссылки полезны и иногда необходимы (подумайте, сколько проблем может возникнуть, если вы можете или не можете вернуть строку в C ++). Ошибка на самом деле не в существовании нулевых указателей, а в том, как система типов обрабатывает их. К сожалению, большинство языков (C ++, Java, C #) не обрабатывают их правильно.

Даниэль Литтл
источник
0

Я думаю, это связано с тем, что функциональное программирование очень беспокоит типы , особенно типы, которые сочетают другие типы (кортежи, функции как первоклассные типы, монады и т. Д.), Чем объектно-ориентированное программирование (или, по крайней мере, изначально).

Современные версии языков программирования, о которых я думаю, вы говорите (C ++, C #, Java), все основаны на языках, которые не имели какой-либо формы общего программирования (C, C # 1.0, Java 1). Без этого вы все еще можете испечь некоторую разницу между обнуляемыми и ненулевыми объектами в языке (например, ссылки на C ++, которые не могут быть null, но также ограничены), но это гораздо менее естественно.

svick
источник
Я думаю, что в случае функционального программирования, это случай, когда FP не имеет ссылочных или указательных типов. В FP все является ценностью. И если у вас есть тип указателя, становится легко сказать «указатель на ничто».
Эйфорическая
0

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

Если кто-то хочет убедиться, что нулевая ссылка, которая появляется там, где ее не должно быть, вызовет конкретное исключение, отличное от NullReferenceExceptionэтого, часто необходимо будет включать нулевые проверки повсюду. С другой стороны, простое обеспечение того, что какое-либо исключение произойдет до того, как нулевая ссылка может привести к «повреждению» сверх того, что уже было сделано, часто требует относительно небольшого количества тестов - тестирование обычно требуется только в тех случаях, когда объект будет хранить ссылка, не пытаясь использовать ее, и либо нулевая ссылка перезапишет действительную, либо это заставит другой код неправильно интерпретировать другие аспекты состояния программы. Такие ситуации существуют, но не все так часто; большинство случайных нулевых ссылок попадут очень быстропроверять их или нет .

Supercat
источник
этот пост довольно трудно читать (стена текста). Не могли бы вы изменить его в лучшую форму?
комнат
1
@gnat: это лучше?
суперкат
0

" Maybe," как написано, это конструкция более высокого уровня, чем ноль. Используя больше слов, чтобы определить это, может быть, это «указатель на вещь или указатель на ничто, но компилятору еще не дали достаточно информации, чтобы определить, какая именно». Это заставляет вас постоянно проверять каждое значение, если только вы не создадите спецификацию компилятора, которая достаточно умна, чтобы не отставать от написанного вами кода.

Вы можете сделать реализацию Maybe с языком, который имеет нули легко. C ++ имеет один в виде boost::optional<T>. Сделать эквивалент нуля с Возможно очень сложно. В частности, если у меня есть Maybe<Just<T>>, я не могу присвоить ему значение null (потому что такого понятия не существует), в то время как T**в языке с нулем очень легко назначить значение null. Это заставляет его использовать Maybe<Maybe<T>>, что полностью допустимо, но заставит вас сделать еще много проверок, чтобы использовать этот объект.

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

Корт Аммон - Восстановить Монику
источник