Эрик Липперт высказал очень интересную мысль в своем обсуждении того, почему C # использует тип, null
а не Maybe<T>
тип :
Согласованность системы типов важна; можем ли мы всегда знать, что необнуляемая ссылка ни при каких обстоятельствах не считается недействительной? Как насчет конструктора объекта с ненулевым полем ссылочного типа? Как насчет финализатора такого объекта, где объект завершен, потому что код, который должен был заполнить ссылку, вызвал исключение? Система типов, которая лжет вам о своих гарантиях, опасна.
Это было немного откровением. Эти концепции меня интересуют, и я немного поигрался с компиляторами и системами типов, но я никогда не думал об этом сценарии. Как языки, которые имеют тип Maybe вместо нулевого, обрабатывают такие крайние случаи, как инициализация и восстановление после ошибок, в которых предположительно гарантированная ненулевая ссылка фактически не находится в допустимом состоянии?
источник
Type?
(возможно) иType
(не ноль)Maybe T
, это не должно быть,None
и, следовательно, вы не можете инициализировать его хранилище с нулевым указателем.Ответы:
Эта цитата указывает на проблему, которая возникает, если объявление и назначение идентификаторов (здесь: элементы экземпляра) отделены друг от друга. Как быстрый набросок псевдокода:
Теперь сценарий заключается в том, что во время создания экземпляра будет выдана ошибка, поэтому построение будет прервано до полного создания экземпляра. Этот язык предлагает метод деструктора, который будет запущен до освобождения памяти, например, для ручного освобождения ресурсов, не связанных с памятью. Его также следует запускать на частично построенных объектах, поскольку ресурсы, управляемые вручную, могли быть уже выделены до прекращения построения.
С пустыми значениями деструктор может проверить, была ли переменная назначена как
if (foo != null) foo.cleanup()
. Без нулей объект теперь находится в неопределенном состоянии - каково значениеbar
?Однако эта проблема существует из-за сочетания трех аспектов:
null
или гарантированная инициализация для переменных-членов.let
оператора, как видно из функциональных языков) - это легко было заставить принудительную инициализацию - но ограничивает язык другими способами.Легко выбрать другой дизайн, который не демонстрирует этих проблем, например, всегда объединяя объявление с присваиванием и предлагая языку несколько блоков финализатора вместо одного метода финализации:
Таким образом, проблема заключается не в отсутствии нуля, а в сочетании набора других функций с отсутствием нуля.
Теперь интересный вопрос, почему C # выбрал один дизайн, а не другой. Здесь в контексте цитаты перечислено много других аргументов для нуля в языке C #, которые в основном можно обобщить как «знакомство и совместимость» - и это веские причины.
источник
null
s: порядок финализации не гарантируется из-за возможности ссылочных циклов. Но я думаю, что вашFINALIZE
дизайн также решает, что: еслиfoo
он уже завершен, егоFINALIZE
раздел просто не будет работать.Таким же образом вы гарантируете, что любые другие данные находятся в действительном состоянии.
Можно структурировать семантику и поток управления таким образом, что вы не можете иметь переменную / поле какого-либо типа, не создавая полностью значение для него. Вместо того, чтобы создавать объект и позволять конструктору присваивать «начальные» значения его полям, вы можете только создать объект, указав значения для всех его полей одновременно. Вместо того, чтобы объявлять переменную и затем присваивать начальное значение, вы можете только ввести переменную с инициализацией.
Например, в Rust вы создаете объект типа структуры через
Point { x: 1, y: 2 }
вместо того, чтобы писать конструктор, который это делаетself.x = 1; self.y = 2;
. Конечно, это может противоречить стилю языка, который вы имеете в виду.Другой дополнительный подход - использование анализа живучести для предотвращения доступа к хранилищу до его инициализации. Это позволяет объявлять переменную без немедленной ее инициализации, если она назначается перед первым чтением. Это может также поймать некоторые связанные с отказом случаи как
Технически, вы также можете определить произвольную инициализацию по умолчанию для объектов, например, обнулить все числовые поля, создать пустые массивы для полей массива и т. Д., Но это довольно произвольно, менее эффективно, чем другие параметры, и может маскировать ошибки.
источник
Вот как это делает Haskell: (не совсем противоречит заявлениям Липперта, поскольку Haskell не является объектно-ориентированным языком).
ПРЕДУПРЕЖДЕНИЕ: длинный обдуманный ответ от серьезного фаната Haskell впереди.
TL; DR
Этот пример точно показывает, насколько Haskell отличается от C #. Вместо того, чтобы делегировать логистику построения конструкции конструктору, она должна обрабатываться в окружающем коде. Нулевое значение (или
Nothing
в Haskell) не может появиться там, где мы ожидаем ненулевое значение, потому что нулевые значения могут встречаться только в специальных вызываемых типах-обертках,Maybe
которые не взаимозаменяемы с / напрямую преобразуются в обычные, не обнуляемые типы. Чтобы использовать значение, которое можно обнулять, заключив его в aMaybe
, мы должны сначала извлечь значение, используя сопоставление с образцом, что заставляет нас перенаправить поток управления в ветвь, где мы точно знаем, что у нас есть ненулевое значение.Следовательно:
Да.
Int
иMaybe Int
два совершенно разных типа. НахождениеNothing
на равнинеInt
было бы сравнимо с нахождением строки «рыба» вInt32
.Не проблема: конструкторы значений в Haskell ничего не могут сделать, кроме как взять значения, которые они дали, и сложить их вместе. Вся логика инициализации происходит до вызова конструктора.
В Хаскеле нет финализаторов, поэтому я не могу решить эту проблему. Мой первый ответ все еще остается, однако.
Полный ответ :
Haskell не имеет значения NULL и использует
Maybe
тип данных для представления значений NULL. Может быть, это тип данных algabraic, определенный следующим образом:Для тех из вас знакомы с Haskell, читать это как «
Maybe
это либоNothing
илиJust a
». В частности:Maybe
является конструктором типа : его можно (неправильно) рассматривать как универсальный класс (гдеa
- переменная типа). Аналогия с C # естьclass Maybe<a>{}
.Just
является конструктором значения : это функция, которая принимает один аргумент типаa
и возвращает значение типа,Maybe a
которое содержит значение. Таким образом, кодx = Just 17
аналогиченint? x = 17;
.Nothing
это другой конструктор значений, но он не принимает аргументов, аMaybe
возвращаемое не имеет значения, кроме «Nothing».x = Nothing
является аналогомint? x = null;
(предполагая, что мы ограниченыa
в HaskellInt
, что можно сделать, написавx = Nothing :: Maybe Int
).Теперь, когда основы этого
Maybe
типа находятся вне пути, как Haskell избегает вопросов, обсуждаемых в вопросе OP?Что ж, Haskell действительно отличается от большинства языков, которые обсуждались до сих пор, поэтому я начну с объяснения нескольких основных принципов языка.
Во-первых, в Хаскеле все неизменно . Все. Имена относятся к значениям, а не к областям памяти, в которых можно хранить значения (это само по себе является огромным источником устранения ошибок). В отличие от C #, где объявление переменной и присваивание две отдельные операции, в Haskell значения создаются путем определения их стоимости (например
x = 15
,y = "quux"
,z = Nothing
), который никогда не может изменить. Следовательно, код вроде:В Хаскеле это невозможно. Нет проблем с инициализацией значений,
null
потому что все должно быть явно инициализировано значением, чтобы оно существовало.Во-вторых, Haskell не является объектно-ориентированным языком : это чисто функциональный язык, поэтому в строгом смысле этого слова нет объектов. Вместо этого есть просто функции (конструкторы значений), которые принимают свои аргументы и возвращают объединенную структуру.
Далее нет абсолютно никакого императивного кода стиля. Под этим я подразумеваю, что большинство языков следуют шаблону примерно так:
Поведение программы выражается в виде серии инструкций. В объектно-ориентированных языках объявления классов и функций также играют огромную роль в потоке программы, но, по сути, «мясо» выполнения программы принимает форму последовательности инструкций, которые должны быть выполнены.
В Хаскеле это невозможно. Вместо этого выполнение программы полностью определяется цепочечными функциями. Даже императивно-выглядящая
do
нотация является просто синтаксическим сахаром для передачи анонимных функций>>=
оператору. Все функции имеют вид:Где
body-expression
может быть что-либо, что оценивается в значение. Очевидно, что доступно больше синтаксических функций, но главное - полное отсутствие последовательностей операторов.Наконец, и, возможно, самое главное, система типов Хаскелла невероятно строгая. Если бы мне пришлось суммировать основную философию проектирования системы типов Haskell, я бы сказал: «Сделайте так, чтобы как можно больше вещей не работало во время компиляции, чтобы как можно меньше работало во время выполнения». Не существует никаких неявных преобразований (хотите продвигать
Int
к aDouble
? ИспользуйтеfromIntegral
функцию). Единственное, что может иметь недопустимое значение во время выполнения, - это использоватьPrelude.undefined
(который, очевидно, просто должен быть там и его невозможно удалить ).Имея все это в виду, давайте посмотрим на «сломанный» пример amon и попытаемся повторно выразить этот код в Haskell. Во-первых, объявление данных (с использованием синтаксиса записи для именованных полей):
(
foo
иbar
действительно являются функциями доступа к анонимным полям здесь, а не к фактическим полям, но мы можем игнорировать эту деталь).Конструктор
NotSoBroken
значения не способен предпринимать какие-либо действия, кроме взятия aFoo
и aBar
(которые не могут быть обнуляемыми), и создаватьNotSoBroken
их из них. Там нет места, чтобы поставить императивный код или даже вручную назначить поля. Вся логика инициализации должна происходить в другом месте, скорее всего, в специальной фабричной функции.В приведенном примере конструкция
Broken
всегда терпит неудачу. Нет никакого способа сломатьNotSoBroken
конструктор значений подобным образом (просто некуда писать код), но мы можем создать фабричную функцию, которая будет аналогично дефектной.(первая строка - объявление сигнатуры типа:
makeNotSoBroken
принимает аргументы aFoo
и aBar
и выдает aMaybe NotSoBroken
).Тип возвращаемого значения должен быть,
Maybe NotSoBroken
а не простоNotSoBroken
потому, что мы указали его для оценкиNothing
, который является конструктором значенияMaybe
. Типы просто не будут выстраиваться, если мы напишем что-то другое.Помимо того, что эта функция абсолютно бессмысленна, эта функция даже не выполняет своего реального предназначения, как мы увидим, когда попробуем ее использовать. Давайте создадим функцию с именем,
useNotSoBroken
которая ожидаетNotSoBroken
в качестве аргумента:(
useNotSoBroken
принимаетNotSoBroken
в качестве аргумента a и выдает aWhatever
).И используйте это так:
В большинстве языков такое поведение может вызвать исключение нулевого указателя. В Haskell типы не совпадают:
makeNotSoBroken
возвращает aMaybe NotSoBroken
, ноuseNotSoBroken
ожидает aNotSoBroken
. Эти типы не являются взаимозаменяемыми, и код не компилируется.Чтобы обойти это, мы можем использовать
case
оператор для ветвления на основе структурыMaybe
значения (используя функцию, называемую сопоставлением с шаблоном ):Очевидно, этот фрагмент должен быть помещен в некоторый контекст для фактической компиляции, но он демонстрирует основы того, как Haskell обрабатывает обнуляемые значения. Вот пошаговое объяснение приведенного выше кода:
makeNotSoBroken
оценивается, что гарантированно выдает значение типаMaybe NotSoBroken
.case
Заявление проверяет структуру этого значения.Nothing
, код «обрабатывать ситуацию здесь» оценивается.Just
значением, выполняется другая ветвь. Обратите внимание, что соответствующее предложение одновременно идентифицирует значение какJust
конструкцию и привязывает его внутреннееNotSoBroken
поле к имени (в данном случае,x
).x
затем можно использовать как нормальноеNotSoBroken
значение, которое есть.Таким образом, сопоставление с образцом обеспечивает мощное средство для обеспечения безопасности типов, поскольку структура объекта неразрывно связана с ветвлением управления.
Я надеюсь, что это было понятное объяснение. Если это не имеет смысла, прыгайте в Learn You A Haskell For Great Good! , один из лучших онлайн языковых уроков, которые я когда-либо читал. Надеюсь, вы увидите ту же красоту на этом языке, что и я.
источник
Я думаю, что ваша цитата - аргумент соломенного чучела.
Современные языки сегодня (включая C #) гарантируют, что конструктор либо полностью завершен, либо нет.
Если в конструкторе есть исключение, и объект оставлен частично неинициализированным, то наличие
null
илиMaybe::none
неинициализированное состояние не имеет реальной разницы в коде деструктора.Вам просто придется иметь дело с этим в любом случае. Когда есть внешние ресурсы для управления, вы должны управлять ими явно любым способом. Языки и библиотеки могут помочь, но вы должны подумать об этом.
Кстати: в C #
null
значение в значительной степени эквивалентноMaybe::none
. Вы можете назначитьnull
только переменным и членам объекта, которые на уровне типа объявлены как обнуляемые :Это ничем не отличается от следующего фрагмента:
Итак, в заключение, я не вижу, насколько обнуляемость каким-либо образом противоположна
Maybe
типам. Я бы даже предположил, что C # пробрался в свой собственныйMaybe
тип и назвал егоNullable<T>
.С помощью методов расширения легко очистить Nullable, следуя монадическому шаблону:
источник
null
неявным членом каждого типа и темMaybe<T>
, что будет сMaybe<T>
, вы также можете иметь простоT
, который не имеет никакого значения по умолчанию.null
не является неявным членом каждого типа. Для того,null
чтобы быть лебальным значением, вам нужно определить тип, который должен быть обнуляемым явно, что делаетT?
(синтаксический сахар дляNullable<T>
) по существу эквивалентнымMaybe<T>
.C ++ делает это, имея доступ к инициализатору, который находится перед телом конструктора. C # запускает инициализатор по умолчанию перед телом конструктора, он приблизительно присваивает 0 всем,
floats
становится 0.0,bools
становится ложным, ссылки становятся нулевыми и т. Д. В C ++ вы можете заставить его запускать другой инициализатор, чтобы гарантировать, что ненулевой ссылочный тип никогда не будет нулевым ,источник
null
, и единственный способ указать отсутствие значения - это использоватьMaybe
тип (также известный какOption
), которого AFAIK C ++ не имеет в стандартная библиотека. Отсутствие нулей позволяет нам гарантировать, что поле всегда будет действительным как свойство системы типов . Это более надежная гарантия, чем ручная проверка на отсутствие пути к коду, где переменная все еще может бытьnull
.