Ответ 1
Андерс Хейлсберг, "отец С#", просто рассказал об этом в
static string AcceptNotNullObject(object! s)
{
return s.ToString();
}
Мы заметили, что множество ошибок в нашем программном обеспечении, разработанном на С# (или Java), вызывают исключение NullReferenceException.
Есть ли причина, по которой "нуль" даже был включен в язык?
В конце концов, если бы не было "null", у меня не было бы ошибки, верно?
Другими словами, какая функция на языке не могла работать без нулевого?
Андерс Хейлсберг, "отец С#", просто рассказал об этом в
static string AcceptNotNullObject(object! s)
{
return s.ToString();
}
Nullity является естественным следствием ссылочных типов. Если у вас есть ссылка, она должна ссылаться на некоторый объект - или быть нулевым. Если бы вы запретили недействительность, вам всегда нужно было убедиться, что каждая переменная была инициализирована некоторым непустым выражением - и даже тогда у вас возникнут проблемы, если переменные были прочитаны во время фазы инициализации.
Как бы вы предложили удалить концепцию nullity?
Как и в объектно-ориентированном программировании, все возвращается к ALGOL. Тони Хоар просто назвал это своей" ошибкой в миллиард долларов ". Во всяком случае, это преуменьшение.
Вот действительно интересный тезис о том, как сделать nullability не стандартным в Java. Параллели с С# очевидны.
Null в С# в основном переносится с С++, у которого были указатели, которые не указывали на ничего в памяти (точнее, адрес 0x00
). В это интервью, Андерс Хейлсберг говорит, что он хотел бы добавить не- нулевые ссылочные типы в С#.
У Null также есть законное место в системе типов, однако, как нечто похожее на нижний тип (где object
- это top). В lisp нижний тип NIL
, а в Scala - Nothing
.
Было бы возможно спроектировать С# без каких-либо нулей, но тогда вам придётся найти приемлемое решение для обычаев, которые обычно имеют для null
, например unitialized-value
, not-found
, default-value
, undefined-value
и None<T>
. Вероятно, было бы меньше усыновления среди программистов на С++ и Java, если бы они так или иначе преуспели в этом. По крайней мере, пока они не увидели, что в программах на С# никогда не было исключений с нулевым указателем.
Удаление нулевого значения не решит. Для большинства переменных, которые установлены в init, вам понадобится ссылка по умолчанию. Вместо исключений с нулевой ссылкой вы получите неожиданное поведение, потому что переменная указывает на неправильные объекты. По крайней мере, нулевые ссылки быстро ускоряются, а не вызывают неожиданное поведение.
Вы можете посмотреть шаблон нулевого объекта для решения части этой проблемы.
Null - чрезвычайно мощная функция. Что вы делаете, если у вас нет значения? Это NULL!
Одна школа мысли никогда не должна возвращать нуль, другая - всегда. Например, некоторые говорят, что вы должны вернуть действительный, но пустой объект.
Я предпочитаю null, поскольку для меня это более точное указание на то, что на самом деле. Если я не могу получить объект из моего уровня персистентности, я хочу null. Мне не нужно пустое значение. Но это я.
Это особенно удобно с примитивами. Например, если у меня есть истина или ложь, но она используется в форме безопасности, где разрешено разрешать, отклонять или не устанавливать. Ну, я хочу, чтобы это не было нулевым. Поэтому я могу использовать bool?
Есть намного больше, о чем я мог бы продолжить, но я оставлю его там.
В конце концов, если бы не было "нулевого", у меня не было бы ошибки, верно?
Ответ НЕТ. Проблема не в том, что С# допускает null, проблема в том, что у вас есть ошибки, которые проявляются с помощью исключения NullReferenceException. Как уже было сказано, нули имеют цель на языке указывать либо "пустой" ссылочный тип, либо не значение (пустое/ничего/неизвестное).
Null не вызывает NullPointerExceptions...
Программисты вызывают NullPointerExceptions.
Без нулей мы вернемся к использованию фактического произвольного значения, чтобы определить, что возвращаемое значение функции или метода было неверным. Вы по-прежнему должны проверять возвращаемый -1 (или что-то еще), удаление нулей не волшебным образом разрешит ленивость, а мучительно запутывает его.
Вопрос может быть истолкован как "Лучше ли иметь значение по умолчанию для каждого типа ссылок (например, String.Empty) или null?". В этом случае я предпочел бы иметь нули, потому что:
"Нуль" включен в язык, потому что у нас есть типы значений и ссылочные типы. Это, вероятно, побочный эффект, но хороший, я думаю. Это дает нам большую власть над тем, как мы эффективно управляем памятью.
Почему у нас есть нуль?...
Типы значений хранятся в "стеке", их значение находится непосредственно в этой части памяти (т.е. int x = 5 означает, что в ячейке памяти для этой переменной содержится "5" ).
Ссылочные типы, с другой стороны, имеют "указатель" в стеке, указывающий на значение фактическое в куче (т.е. строка x = "ello" означает, что блок памяти в стеке содержит только адрес, указывающий на фактическое значение в куче).
Значение null просто означает, что наше значение в стеке не указывает на какое-либо фактическое значение в куче - это пустой указатель.
Надеюсь, я объяснил это достаточно хорошо.
Бывают ситуации, когда нуль является хорошим способом означать, что ссылка не была инициализирована. Это важно в некоторых сценариях.
Например:
MyResource resource;
try
{
resource = new MyResource();
//
// Do some work
//
}
finally
{
if (resource != null)
resource.Close();
}
В большинстве случаев это достигается с помощью оператора using. Но шаблон все еще широко используется.
Что касается вашего исключения NullReferenceException, причиной таких ошибок часто бывает легко уменьшить, внедряя стандарт кодирования, где все параметры a проверены на достоверность. В зависимости от характера проекта я нахожу, что в большинстве случаев достаточно проверить параметры на открытых участках. Если параметры не находятся в пределах ожидаемого диапазона, генерируется исключение ArgumentException или возвращается результат ошибки в зависимости от используемого шаблона обработки ошибок.
Проверка параметров сама по себе не удаляет ошибки, но любые возникающие ошибки легче найти и исправить на этапе тестирования.
В качестве примечания Андерс Хейлсберг упомянул отсутствие ненулевого применения в качестве одной из самых больших ошибок в спецификации С# 1.0 и что в настоящее время это "сложно".
Если вы все еще думаете, что статически исполнение ненулевым эталонное значение имеет большое значение можно проверить спецификации # язык. Это расширение С#, где непустые ссылки являются частью языка. Это гарантирует, что ссылка, помеченная как ненулевая, никогда не может быть назначена нулевой ссылкой.
Если вы получаете "NullReferenceException", возможно, вы продолжаете ссылаться на объекты, которые больше не существуют. Это не проблема с "null", это проблема с вашим кодом, указывающим на несуществующие адреса.
Null, поскольку он доступен в С#/С++/Java/Ruby, лучше всего рассматривать как странность какого-то неясного прошлого (Algol), который каким-то образом сохранился и по сей день.
Вы используете его двумя способами:
Как вы догадались, 1) это то, что заставляет нас бесконечно беспокоиться об общих императивных языках и должно было быть давно запрещено, 2) является истинной важной особенностью.
Существуют языки, которые избегают 1) без предупреждения 2).
Например OCaml - это такой язык.
Простая функция, возвращающая любое возрастающее целое число, начиная с 1:
let counter = ref 0;;
let next_counter_value () = (counter := !counter + 1; !counter);;
И относительно опциональности:
type distributed_computation_result = NotYetAvailable | Result of float;;
let print_result r = match r with
| Result(f) -> Printf.printf "result is %f\n" f
| NotYetAvailable -> Printf.printf "result not yet available\n";;
В одном ответе упоминается, что в базах есть нули. Это правда, но они сильно отличаются от нулей в С#.
В С# нули являются маркерами для ссылки, которая не ссылается ни на что.
В базах данных нулями являются маркеры для ячеек значений, которые не содержат значения. В ячейках значений я обычно имею в виду пересечение строки и столбца в таблице, но концепция ячеек значений может быть расширена за пределами таблиц.
Разница между двумя кажется тривиальной, поначалу. Но это не так.
Я не могу говорить с вашей конкретной проблемой, но похоже, что проблема заключается не в существовании null. Null существует в базах данных, вам нужно каким-то образом учесть это на уровне приложения. Я не думаю, что единственная причина, по которой она существует в .net, заметьте. Но я считаю это одной из причин.
Я удивлен, что никто не говорил о базах данных для своего ответа. Базы данных имеют поля с нулевым значением, и любой язык, который будет получать данные из БД, должен справиться с этим. Это означает наличие нулевого значения.
На самом деле это так важно, что для базовых типов типа int вы можете сделать их обнуляемыми!
Также рассмотрите возвращаемые значения из функций, что, если вы хотите, чтобы функция разделила пару чисел, а знаменатель мог быть 0? Единственный "правильный" ответ в таком случае был бы нулевым. (Я знаю, в таком простом примере исключение, скорее всего, будет лучшим вариантом... но могут быть ситуации, когда все значения верны, но действительные данные могут приводить к недопустимому или невосполнимому ответу. Не уверен, что исключение должно использоваться в таких случаи...)
Помимо всех упомянутых причин, NULL необходим, когда вам нужен заполнитель для еще не созданного объекта. Например. если у вас есть круговая ссылка между парой объектов, тогда вам нужно null, так как вы не можете создавать экземпляры одновременно.
class A {
B fieldb;
}
class B {
A fielda;
}
A a = new A() // a.fieldb is null
B b = new B() { fielda = a } // b.fielda isnt
a.fieldb = b // now it isnt null anymore
Изменить: вы можете вытащить язык, который работает без нулей, но это определенно не будет объектно-ориентированным языком. Например, пролог не имеет нулевых значений.
Если вы создаете объект с переменной экземпляра, являющейся ссылкой на какой-либо объект, какое значение вы предложите иметь эту переменную, прежде чем назначить ссылку на какой-либо объект?
Я предлагаю:
Обычно - исключение NullReferenceException означает, что какой-то метод не понравился, что он был передан, и возвратил нулевую ссылку, которая позже использовалась без проверки ссылки перед использованием.
Этот метод может иметь более подробное исключение вместо того, чтобы возвращать нуль, что соответствует отказу быстрого режима мышления.
Или метод может возвращать null как удобство для вас, так что вы можете написать, если вместо try и избежать "служебных" исключений.
Являются ли эти случаи использования или злоупотребления субъективными, но иногда я их использую.
Если структура позволяет создавать массив какого-либо типа без указания того, что должно быть сделано с новыми элементами, этот тип должен иметь какое-то значение по умолчанию. Для типов, реализующих изменчивую ссылочную семантику (*), в общем случае нет разумного значения по умолчанию. Я считаю слабостью платформы .NET, что нет способа указать, что вызов не виртуальной функции должен подавлять любую нулевую проверку. Это позволило бы неизменным типам, например String, вести себя как типы значений, возвращая разумные значения для таких свойств, как Length.
(*) Обратите внимание, что в VB.NET и С# изменчивая ссылочная семантика может быть реализована либо классами, либо типами структуры; тип структуры будет реализовывать изменчивую ссылочную семантику, действуя как прокси для обернутого экземпляра объекта класса, к которому он содержит неизменяемую ссылку.
Также было бы полезно, если бы можно было указать, что класс должен иметь непутевую переменную семантику значения типа (подразумевая, что - при минимальном - создание экземпляра поля этого типа создало бы новый экземпляр объекта, используя конструктор по умолчанию, и что копирование поля этого типа создаст новый экземпляр, скопировав старый (рекурсивно обрабатывая любые классы вложенного типа).
Однако неясно, сколько именно поддержки должно быть встроено в рамки этого. Если сама структура распознает различия между изменяемыми типами значений, изменяемыми ссылочными типами и неизменяемыми типами, они позволят классам, которые сами содержат ссылки на переменные и неизменяемые типы из внешних классов, чтобы эффективно избежать ненужных копий глубоко неизменных объектов.
Извините за ответ, опоздавший на четыре года, я поражен тем, что ни один из ответов до сих пор не ответил на исходный вопрос таким образом:
Языки, такие как С# и Java, например C и другие языки перед ними, имеют null
, так что программист может быстро писать, оптимизировать код, используя эффективные указатели.
Немного истории. Причина, по которой null
была изобретена, - это эффективность. При выполнении низкоуровневого программирования в сборке нет абстракции, у вас есть значения в регистрах, и вы хотите максимально использовать их. Определение нуля как значения не действительного указателя - отличная стратегия для представления объекта или ничего.
Зачем тратить большинство возможных значений совершенно хорошего слова памяти, когда у вас может быть накладные расходы с нулевой памятью, очень быстрая реализация шаблона необязательного значения? Вот почему null
настолько полезен.
Семантически, null
не обязательно нужно программировать языки. Например, в классических функциональных языках, таких как Haskell или в семействе ML, нет нулевых, а скорее типов с именем Maybe или Option. Они представляют собой более высокоуровневую концепцию необязательного значения, не будучи каким-либо образом обеспокоены тем, как будет выглядеть сгенерированный код сборки (это будет задание компилятора).
И это тоже очень полезно, потому что он позволяет компилятору улавливать больше ошибок, а это означает меньше NullReferenceExceptions.
В отличие от этих языков программирования высокого уровня, С# и Java допускают возможное значение null для каждого ссылочного типа (это другое имя для типа, которое будет реализовано с помощью указателей).
Это может показаться плохим, но что хорошего в том, что программист может использовать знания о том, как он работает под капотом, для создания более эффективного кода (хотя язык имеет сбор мусора).
Именно по этой причине null
все еще существует в современных языках: компромисс между необходимостью общей концепции необязательной ценности и постоянной потребностью в эффективности.
Функция, которая не может работать без нуля, может представлять "отсутствие объекта".
Отсутствие объекта является важным понятием. В объектно-ориентированном программировании нам это нужно, чтобы представить связь между объектами, которые являются необязательными: объект A может быть присоединен к объекту B, или может не иметь объекта B. Без нуля мы все равно могли бы эмулировать это: например мы могли бы использовать список объектов для связывания B с A. Этот список может содержать один элемент (один B) или быть пустым. Это несколько неудобно и на самом деле ничего не решает. Код, который предполагает, что существует B, например aobj.blist.first().method()
, будет взорваться аналогично исключению нулевой ссылки: (если blist
пуст, каково поведение blist.first()
?)
Говоря о списках, null позволяет завершить связанный список. A ListNode
может содержать ссылку на другую ListNode
, которая может быть нулевой. То же самое можно сказать и о других динамических сетках, таких как деревья. Null позволяет вам иметь обычное двоичное дерево, чьи листовые узлы отмечены наличием дочерних ссылок, которые являются нулевыми.
Списки и деревья могут быть построены без нулевого значения, но они должны быть круговыми или бесконечными/ленивыми. Это, вероятно, будет считаться неприемлемым ограничением для большинства программистов, которые предпочли бы иметь выбор при проектировании структур данных.
Боли, связанные с нулевыми ссылками, такими как пустые ссылки, возникающие случайно из-за ошибок и вызывающих исключения, частично являются следствием системы статического типа, которая вводит нулевое значение в каждый тип: существует нуль String, null Integer, null Widget,...
В динамически типизированном языке может быть один нулевой объект, который имеет свой собственный тип. Результатом этого является то, что у вас есть все преференциальные преимущества, равные нулю, плюс большая безопасность. Например, если вы пишете метод, который принимает параметр String, то вам гарантируется, что этот параметр будет строковым объектом, а не null. В классе String нет нулевой ссылки: то, что известно как String, не может быть объектом null. Ссылки не имеют типа в динамическом языке. Место хранения, такое как член класса или параметр функции, содержит значение, которое может быть ссылкой на объект. Этот объект имеет тип, а не ссылку.
Таким образом, эти языки предоставляют чистую, более или менее математически чистую модель "null", а затем статические превращают ее в нечто вроде монстра Франкенштейна.
Нуль - необходимое требование любого языка OO. Любая объектная переменная, которой не была назначена ссылка на объект, должна быть пустой.