Являются ли нулевые ссылки действительно плохой вещью?


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
Я бы с осторожностью отнесся к подходу «если клиент существует / затем получи клиента», как только вы напишите две строчки такого кода, вы откроете дверь для условий гонки. Получить, а затем проверить наличие исключений null / catch безопаснее.
Carson63000

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

хотя на некоторых языках есть операторы слияния и безопасной навигации
jk.

35
ноль сам по себе не плохо. Система типов, которая не делает различий между типом T и типом T + null, является плохой.
Инго

27
Возвращаясь к моему собственному вопросу несколько лет спустя, теперь я полностью сторонник подхода Option / Maybe.
Тим Гудман

Ответы:


91

ноль это зло

На InfoQ есть презентация на эту тему: « Пустые ссылки: ошибка миллиарда долларов » Тони Хоара

Тип варианта

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

Хорошая статья Шаблон «Option», в котором обсуждается тип Option и предоставляется его реализация для Java.

Я также нашел сообщение об ошибке для Java об этой проблеме: добавьте типы Nice Option в Java для предотвращения исключений NullPointerExceptions . Запрошенная функция была введена в Java 8.


5
Nullable <x> в C # является аналогичной концепцией, но далеко не является реализацией шаблона option. Основная причина в том, что в .NET System.Nullable ограничивается типами значений.
MattDavey

27
+1 Для типа Option. После знакомства с Haskell's Maybe , нули начинают выглядеть странно ...
Андрес Ф.

5
Разработчик может ошибиться где угодно, поэтому вместо исключения нулевого указателя вы получите пустую опцию исключения. Чем последний лучше первого?
Гринольдман

7
@greenoldman нет такой вещи как «исключение пустого параметра». Попробуйте вставить значения NULL в столбцы базы данных, определенные как NOT NULL.
Джонас

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

127

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

Вы правы в том, что изящная обработка ошибок может быть функционально идентична ifоператорам проверки на нуль . Но что происходит, когда то, что вы сами убедили в том, что не может быть нулем, фактически является нулем? Kerboom. Что бы ни случилось дальше, я готов поспорить, что 1) это не будет изящно и 2) вам это не понравится.

И не стоит сбрасывать со счетов значение «легко отлаживать». Зрелый производственный код - безумное, растянутое существо; все, что дает вам более полное представление о том, что пошло не так и где может сэкономить часы копания.


27
+1 Обычно ошибки в производственном коде НЕОБХОДИМО идентифицировать как можно быстрее, чтобы их можно было исправить (обычно ошибка данных). Чем проще отладить, тем лучше.

4
Этот ответ тот же, если применять его к любому из примеров ФП. Либо вы проверяете на наличие ошибок, либо нет, и вы либо исправляете их, либо нет. Другими словами, удаление пустых ссылок из языка не изменяет классы ошибок, которые вы будете испытывать, это лишь слегка изменит проявление этих ошибок.
dash-tom-bang

19
+1 за неразорвавшиеся бомбы. С пустыми ссылками выражение public Foo doStuff()не означает «делать вещи и возвращать результат Foo», это означает «делать вещи и возвращать результат Foo, ИЛИ возвращать ноль, но вы не представляете, произойдет ли это или нет». В результате вы должны проверять NULL везде, чтобы быть уверенным. Можно также удалить пустые значения и использовать специальный тип или шаблон (например, тип значения), чтобы указать бессмысленное значение.
Мэтт Оленик

12
@greenoldman, ваш пример вдвойне эффективен, чтобы продемонстрировать нашу точку зрения о том, что использование примитивных типов (нулевых или целых) в качестве семантических моделей является плохой практикой. Если у вас есть тип, для которого отрицательные числа не являются допустимым ответом, вы должны создать новый класс типов для реализации семантической модели с этим значением. Теперь весь код, предназначенный для моделирования этого неотрицательного типа, локализован в его классе, изолирован от остальной системы, и любая попытка создать для него отрицательное значение может быть немедленно обнаружена.
Гуперникетес

9
@greenoldman, вы продолжаете доказывать, что примитивные типы не подходят для моделирования. Сначала это было по отрицательным числам. Теперь дело за оператором деления, когда делителем является ноль. Если вы знаете, что не можете делить на ноль, вы можете предсказать, что результат не будет хорошим, и будете ответственны за то, что вы проверяете, и предоставили соответствующее «действительное» значение для этого случая. Разработчики программного обеспечения несут ответственность за знание параметров, в которых работает их код, и за правильное кодирование. Это то, что отличает инженерию от мастеринга.
Гуперникетес

50

Есть несколько проблем с использованием нулевых ссылок в коде.

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

Во-вторых, отладка кода становится более сложной, когда появляется нулевая ссылка, и вы пытаетесь определить, что сгенерировало ее , какое состояние действует и его причину, даже если вы можете проследить его восходящий путь выполнения.

В-третьих, нулевые ссылки вводят дополнительные пути кода для тестирования .

В-четвертых, как только нулевые ссылки используются в качестве допустимых состояний для параметров, а также возвращаемых значений, защитное программирование (для состояний, вызванных проектированием) требует более тщательной проверки нулевых ссылок в разных местах ... на всякий случай.

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

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

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


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

Однако данные мусора намного реже, чем нулевые ссылки. Особенно в управляемых языках.
Дин Хардинг

5
-1 для нулевого объекта.
DeadMG

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

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

48

Нули не так уж плохи, если только вы их не ожидаете. Вы должны явно указать в коде, что вы ожидаете null, что является проблемой языкового дизайна. Учти это:

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!"); }

Если вы попытаетесь вызвать методы для a Customer?, вы должны получить ошибку во время компиляции. Причина, по которой другие языки не делают этого (IMO), заключается в том, что они не обеспечивают изменение типа переменной в зависимости от того, в какой области она находится. Если язык может с этим справиться, тогда проблема может быть полностью решена в рамках система типов.

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


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

14
@JBRWilkinson: Это просто готовый пример, демонстрирующий идею, а не пример реальной реализации. Я на самом деле думаю, что это довольно изящная идея.
Дин Хардинг

1
Вы правы, что это не шаблон нулевого объекта.
Дин Хардинг

13
Это похоже на то, что Хаскелл называет Maybe- A Maybe Customer- это Just c(где cесть Customer) или Nothing. На других языках это называется Option- посмотрите другие ответы на этот вопрос.
MatrixFrog

2
@SimonBarker: вы можете быть уверены, что Customer.FirstNameэто тип String(в отличие от String?). Это реальная выгода - только переменные / свойства, которые не имеют значащего значения ничего, должны быть объявлены как обнуляемые.
Йогу

20

Существует множество отличных ответов, которые охватывают неприятные симптомы 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скорее всего, будет иметь свою собственную проверку, просто чтобы быть в безопасности.

Сравните это с использованием типа Maybe/ Optionс сопоставлением с шаблоном:

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 для сопоставления с образцом. Учитывая количество ответов выше, в которых упоминаются типы Option, можно подумать, что кто-то упомянул бы тот факт, что простая языковая функция, такая как сопоставители шаблонов, может сделать их гораздо более интуитивно понятными для работы, чем пустые.
Жюль

1
Я думаю, что монадное связывание даже более полезно, чем сопоставление с образцом (хотя, конечно, связывание для Maybe реализовано с использованием сопоставления с образцом). Я думаю , что это своего рода забавные видящие языки , как C # положить специальный синтаксис для многих вещей ( ??, ?., и так далее) для того , что языки , как Haskell реализуют , как библиотечные функции. Я думаю, что это более аккуратно. null/ Nothingраспространение ни в коем случае не «особенное», так почему мы должны изучать больше синтаксиса, чтобы иметь его?
Сара

14

(Кидаю шляпу в кольцо за старый вопрос;))

Конкретная проблема с нулем заключается в том, что он нарушает статическую типизацию.

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

Таким образом, в некотором смысле, null вводит динамическую типизацию в статически типизированную систему с ожидаемыми результатами.

Разница между этим делением на ноль или логарифмом отрицательного и т. Д. Состоит в том, что когда i = 0, это все еще int. Компилятор все еще может гарантировать его тип. Проблема в том, что логика неправильно применяет это значение таким образом, который не разрешен логикой ... но если логика делает это, это в значительной степени определение ошибки.

( Кстати, компилятор может уловить некоторые из этих проблем. Как помечать выражения, как i = 1 / 0во время компиляции. Но вы не можете ожидать, что компилятор последует за функцией и будет гарантировать, что все параметры соответствуют логике функции)

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

Строка s = новое целое число (1234);

Так почему же он должен позволять присваивать значение (нуль), которое прервет разыменование s?

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


3
В C переменная типа *intлибо идентифицирует int, либо NULL. Аналогично в C ++ переменная типа *Widgetлибо идентифицирует a Widget, вещь, тип которой происходит от Widget, либо NULL. Проблема с Java и производными, такими как C #, заключается в том, что они используют место хранения Widgetдля обозначения *Widget. Решение состоит не в том, чтобы притворяться, что объект *Widgetне должен быть в состоянии удержаться NULL, а в том, чтобы иметь надлежащий Widgetтип места хранения.
суперкат

14

nullУказатель представляет собой инструмент

не враг. Вы просто должны использовать их правильно.

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

Если вы все еще не уверены, добавьте в свой сценарий хорошую дозу многопоточности, а затем подумайте еще раз.

Мораль истории: не бросайте детей в воду. И не вините инструменты в аварии. Человек, который изобрел число ноль давным-давно, был довольно умен, так как в тот день вы могли назвать «ничто». nullУказатель не так далеко от этого.


РЕДАКТИРОВАТЬ: хотя NullObjectшаблон кажется лучшим решением, чем ссылки, которые могут быть нулевыми, он создает проблемы самостоятельно:

  • Ссылка, содержащая a, NullObjectдолжна (согласно теории) ничего не делать при вызове метода. Таким образом, тонкие ошибки могут быть внесены из-за ошибочно неназначенных ссылок, которые теперь гарантированно не равны нулю (да!), Но выполняют нежелательное действие: ничего. С NPE это почти очевидно, где проблема лежит. С a, NullObjectкоторый ведет себя как-то (но неправильно), мы вводим риск обнаружения ошибок (слишком) поздно. Это не случайно похоже на проблему недопустимого указателя и имеет аналогичные последствия: ссылка указывает на что-то, что похоже на действительные данные, но не вызывает ошибки данных и может быть трудно отследить. Честно говоря, в этих случаях я бы без промедления предпочел бы NPE, которая немедленно выходит из строя , сейчас,из-за логической ошибки / ошибки в данных, которая неожиданно ударит меня позже по дороге. Помните исследование IBM о том, как стоимость ошибок зависит от того, на каком этапе они обнаруживаются?

  • Понятие бездействия, когда метод вызывается в a NullObject, не сохраняется, когда вызывается свойство get или функция, или когда метод возвращает значения через out. Допустим, возвращаемое значение является, intи мы решаем, что «ничего не делать» означает «вернуть 0». Но как мы можем быть уверены, что 0 - это правильное «ничто», которое нужно (не) вернуть? В конце концов, он NullObjectне должен ничего делать, но когда запрашивается значение, он должен как-то реагировать. Сбой не вариант: мы используем, NullObjectчтобы предотвратить NPE, и мы, конечно, не будем торговать с другим исключением (не реализовано, делить на ноль, ...), не так ли? Так как же правильно вернуть ничего, когда нужно что-то вернуть?

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


Можете ли вы представить аргумент, почему nullдолжен существовать? Можно просто поменять любой языковой дефект: «Это не проблема, просто не делайте ошибку».
Доваль

На какое значение вы бы указали неверный указатель?
JensG

Ну, вы не устанавливаете их в любое значение, потому что вы не должны их вообще разрешать. Вместо этого вы используете переменную другого типа, возможно, вызванную, Maybe Tкоторая может содержать либо значение Nothing, либо Tзначение. Ключевым моментом здесь является то, что вы вынуждены проверить, Maybeдействительно ли объект содержит, Tпрежде чем вам разрешено его использовать, и предоставить план действий на случай, если его нет. И поскольку вы запретили неверные указатели, теперь вам никогда не нужно проверять что-либо, что не является Maybe.
Доваль

1
@JensG в вашем последнем примере, компилятор заставляет вас выполнять проверку на ноль перед каждым вызовом t.Something()? Или есть какой-то способ узнать, что вы уже сделали проверку, и не заставлять вас делать это снова? Что делать, если вы сделали проверку, прежде чем tперейти к этому методу? С типом Option компилятор узнает, выполнили ли вы проверку или нет, посмотрев на тип t. Если это Option, вы не проверили его, а если это развернутое значение, значит, у вас есть.
Тим Гудман

1
Что возвращает нулевой объект при запросе значения?
JensG

8

Нулевые ссылки являются ошибкой, поскольку они допускают бессмысленный код:

foo = null
foo.bar()

Есть альтернативы, если вы используете систему типов:

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

Основная идея состоит в том, чтобы поместить переменную в коробку, и единственное, что вы можете сделать, - это распаковать ее, предпочтительно заручиться помощью компилятора, как предложено для Eiffel.

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


6
-1 за «плечо»!
Марк С

8
Но, конечно же, многие общие программные конструкции допускают бессмысленный код, не так ли? Деление на ноль (возможно) бессмысленно, но мы все же разрешаем деление и надеемся, что программист не забудет проверить, что делитель ненулевой (или обработать исключение).
Тим Гудман

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

4
@FredOverflow: так как прелюдия загружается по умолчанию, я бы посчитал «с нуля», что у Haskell есть достаточно удивительная система типов, позволяющая определять эти (и другие) тонкости с общими правилами языка, это просто бонус :)
Матье М.

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

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. что ваши защитные механизмы действительно эффективно справляются со всеми потенциальными NPE.

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


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
}

Когда ссылки на классы передаются, защитное программирование побуждает вас снова проверять все параметры на null, даже если вы только что проверили их на null прямо перед этим, особенно при создании тестируемых модулей многократного использования. Одна и та же ссылка может быть легко проверена на нулевое значение 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.


Я думаю, что другим важным моментом в Option <T> является то, что это монада (и, следовательно, также endofunctor), так что вы можете составлять сложные вычисления для потоков значений, содержащих необязательные типы, без явной проверки для Noneкаждого шага пути.
Сара

3

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

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

Что делает поиск Гудман, когда ее там нет? Без нуля вы должны вернуть своего рода клиента по умолчанию - и теперь вы продаете вещи по этому умолчанию.

По сути, все сводится к тому, что более важно, чтобы код работал независимо от того, что или он работает правильно. Если неправильное поведение предпочтительнее, чем отсутствие поведения, то вам не нужны значения NULL. (В качестве примера того, как это может быть правильным выбором, рассмотрим первый запуск Ariane V. Ошибка uncaught / 0 заставила ракету совершить крутой поворот, и саморазрушение сработало, когда она развалилась из-за этого. Значение он пытался вычислить, что на самом деле больше не служит какой-либо цели в тот момент, когда зажигается ракета-носитель - он бы вышел на орбиту, несмотря на обычную процедуру возврата мусора.)

По крайней мере, 99 раз из 100 я бы выбрал использовать нули. Я бы прыгнул от радости, заметив их собственную версию.


1
Это хороший ответ. Нулевая конструкция в программировании не является проблемой, это все экземпляры нулевых значений. Если вы создадите объект по умолчанию, в конечном итоге все ваши значения NULL будут заменены этими значениями по умолчанию, и вместо этого ваш пользовательский интерфейс, БД и все, что находится между ними, будут заполнены теми, которые, вероятно, больше не нужны. Но вы будете спать по ночам, потому что, по крайней мере, ваша программа не падает, я думаю.
Крис Макколл

2
Вы предполагаете, что nullэто единственный (или даже предпочтительный) способ моделирования отсутствия значения.
Сара

1

Оптимизировать для наиболее распространенного случая.

Необходимость постоянно проверять на ноль утомительна - вы хотите иметь возможность просто получить объект Customer и работать с ним.

В обычном случае это должно работать просто отлично - вы делаете поиск, получаете объект и используете его.

В исключительном случае , когда вы (случайным образом) ищете клиента по имени, не зная, существует ли эта запись / объект, вам понадобится некоторое указание на то, что это не удалось. В этой ситуации ответ - вызвать исключение RecordNotFound (или позволить провайдеру SQL сделать это за вас).

Если вы находитесь в ситуации, когда вы не знаете, можете ли вы доверять входящим данным (параметру), возможно, из-за того, что они были введены пользователем, вы также можете предоставить «TryGetCustomer (имя, клиент»). шаблон. Перекрестная ссылка с int.Parse и int.TryParse.


Я бы сказал, что вы никогда не знаете, можете ли вы доверять поступающим данным, потому что они поступают в функцию и имеют тип NULL. Код может быть изменен, чтобы сделать ввод совершенно другим. Так что если есть ноль, вы всегда должны проверить. Таким образом, вы получаете систему, в которой каждая переменная проверяется на нулевое значение каждый раз, прежде чем к ней обращаются. Случаи, когда он не проверен, становятся процентом отказов вашей программы.
Крис Макколл

0

Исключения не оценены!

Они там по причине. Если ваш код работает плохо, есть шаблон гениальности, называемый исключением , и он говорит вам, что что-то не так.

Избегая использования пустых объектов, вы скрываете часть этих исключений. Я не зацикливался на примере OP, где он преобразует исключение нулевого указателя в хорошо типизированное исключение, на самом деле это может быть хорошей вещью, поскольку это повышает удобочитаемость. Однако, когда вы принимаете решение типа Option, как указывал @Jonas, вы скрываете исключения.

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

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


1
Теперь я знаю немного больше о типе Option, чем когда я публиковал вопрос несколько лет назад, поскольку теперь я регулярно использую их в Scala. Смысл Option заключается в том, что он превращает присвоение Noneчего-то, что не является Option, в ошибку времени компиляции, и аналогичным образом использует Optionas, как будто оно имеет значение без предварительной проверки, что это ошибка времени компиляции. Ошибки компиляции, безусловно, лучше, чем исключения во время выполнения.
Тим Гудман

Например: ты не можешь сказать 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?") }
Тим Гудман

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

2
Однако я согласен с общим замечанием, что исключения (используемые должным образом) не являются плохой вещью, и «исправление» исключения путем его проглатывания обычно является ужасной идеей. Но с типом Option цель состоит в том, чтобы превратить исключения в ошибки времени компиляции, что лучше.
Тим Гудман

@TimGoodman - это тактика scala only или ее можно использовать в других языках, таких как 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заявления.

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


0

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

Итак, лучший из моих советов создателям и дизайнерам ADA был проигнорирован. .... Не допускайте использования этого языка в его нынешнем состоянии в приложениях, где надежность имеет решающее значение, например, на атомных электростанциях, крылатых ракетах, системах раннего предупреждения, системах противоракетной обороны. Следующая ракета, сбившаяся с пути в результате ошибки языка программирования, может не быть исследовательской космической ракетой в безобидном путешествии на Венеру: это может быть ядерная боеголовка, взрывающаяся над одним из наших городов. Ненадежный язык программирования, генерирующий ненадежные программы, представляет гораздо больший риск для нашей окружающей среды и нашего общества, чем небезопасные автомобили, токсичные пестициды или аварии на атомных электростанциях. Будьте бдительны, чтобы уменьшить риск, а не увеличить его.

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

Разработчик обязан заключать контракты между клиентом и поставщиком. Например, в C #, как и в Java, вы можете использовать, Genericsчтобы минимизировать влияние Nullссылки, создавая readonly 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
}

Такой подход может значительно повысить надежность приложений и качество программного обеспечения. Проектирование по контракту - это случай логики Хоара , который был заполнен Бертраном Мейером в его знаменитой книге «Построение объектно-ориентированного программного обеспечения» и язык Eiffel в 1988 году, но он недопустимо используется в современной разработке программного обеспечения.


0

Нет.

Неспособность языка учитывать такие особые значения, как nullпроблема языка. Если вы посмотрите на такие языки, как Kotlin или Swift , которые заставляют автора явно иметь дело с возможностью нулевого значения при каждом столкновении, то опасности нет.

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


-1

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

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


-1

У меня есть одно простое возражение против null:

Он полностью нарушает семантику вашего кода, внося двусмысленность .

Часто вы ожидаете, что результат будет определенного типа. Это семантически обоснованно. Скажем, вы запросили базу данных для пользователя с определенным id, вы ожидаете, что результат будет определенного типа (= user). Но что, если нет пользователя с таким идентификатором? Одно (плохое) решение: добавление nullзначения. Так что результат неоднозначен : либо есть, userлибо есть null. Но nullэто не ожидаемый тип. И вот тут начинается запах кода .

Во избежание nullэтого вы делаете свой код семантически понятным.

Есть всегда способы вокруг null: рефакторинга коллекциям, Null-objects, и optionalsт.д.


-2

Если семантически возможно будет получить доступ к хранилищу ссылки или типа указателя до того, как будет выполнен код для вычисления значения для него, не существует идеального выбора относительно того, что должно произойти. Наличие в таких местах по умолчанию нулевых ссылок на указатели, которые могут быть свободно прочитаны и скопированы, но которые потерпят неудачу в случае разыменования или индексации, являются одним из возможных вариантов. Другие варианты включают в себя:

  1. По умолчанию местоположения имеют нулевой указатель, но не позволяют коду просматривать их (или даже определять, что они нулевые) без сбоев. Возможно, предоставить явное средство установки указателя на ноль.
  2. По умолчанию для местоположений задан нулевой указатель, и происходит сбой, если была предпринята попытка прочитать указатель, но при этом предусмотрены средства, позволяющие проверить, не содержит ли указатель нулевой указатель. Также предоставьте явные средства установки указателя на ноль.
  3. У местоположений по умолчанию указатель на некоторый конкретный предоставленный компилятором экземпляр по умолчанию указанного типа.
  4. У местоположений по умолчанию указатель на какой-то конкретный предоставленный программой экземпляр по умолчанию указанного типа.
  5. У любого доступа с нулевым указателем (не только операции разыменования) вызовите некоторую программную подпрограмму для предоставления экземпляра.
  6. Разработайте семантику языка таким образом, чтобы коллекции мест хранения не существовали, за исключением временного недоступного компилятора, до тех пор, пока подпрограммы инициализации не будут выполнены для всех членов (что-то вроде конструктора массива должно быть предоставлено либо с экземпляром по умолчанию, функция, которая будет возвращать экземпляр, или, возможно, пару функций - одна для возврата экземпляра и одна, которая будет вызываться для ранее созданных экземпляров, если при создании массива возникает исключение).

Последний вариант может иметь некоторую привлекательность, особенно если язык включает в себя типы, допускающие как nullable, так и non-nullable (можно вызывать специальные конструкторы массива для любых типов, но их нужно вызывать только при создании массивов ненулевых типов), но, вероятно, не было бы осуществимо в то время, когда были изобретены нулевые указатели. Из других вариантов ни один из них не выглядит более привлекательным, чем копирование нулевых указателей, но не разыменование или индексация. Подход № 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.