Об обсуждениях, не связанных с нулевыми типами

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

РЕДАКТИРОВАТЬ: Игнорировать мой комментарий о SpeС#. Я неправильно понял, как это работает.

РЕДАКТИРОВАТЬ 2: Я должен разговаривать с неправильными людьми, я действительно надеялся, чтобы кто-то спорил с: -)


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

class Class { ... }

void main() {
    Class c = nullptr;
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

BAM! Нарушение доступа. Кто-то забыл инициализировать c.


Теперь рассмотрим следующее:

class Class { ... }

void main() {
    Class c = new Class(); // set to new Class() by default
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

Упс. Петля тихо пропущена. Это может занять некоторое время, чтобы выявить проблему.


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

Ответ 1

Немного странно, что ответ, помеченный как "ответ" в этом потоке, на самом деле выделяет проблему с нулем, в первую очередь, а именно:

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

Не было бы неплохо, если бы компилятор мог поймать такие ошибки во время компиляции, а не во время выполнения?

Если вы использовали ML-подобный язык (SML, OCaml, SML и F # в некоторой степени) или Haskell, ссылочные типы не являются нулевыми. Вместо этого вы представляете "нулевое" значение, обертывая его типом параметра. Таким образом, вы фактически меняете возвращаемый тип функции, если он может вернуть значение null в качестве юридического значения. Итак, скажем, я хотел вытащить пользователя из базы данных:

let findUser username =
    let recordset = executeQuery("select * from users where username = @username")
    if recordset.getCount() > 0 then
        let user = initUser(recordset)
        Some(user)
    else
        None

Найти пользователя имеет тип val findUser : string -> user option, поэтому возвращаемый тип функции фактически говорит вам, что он может вернуть нулевое значение. Чтобы потреблять код, вам необходимо обрабатывать как случаи Some, так и None:

match findUser "Juliet Thunderwitch" with
| Some x -> print_endline "Juliet exists in database"
| None -> print_endline "Juliet not in database"

Если вы не обрабатываете оба случая, код даже не компилируется. Таким образом, система типов гарантирует, что вы никогда не получите исключение с помощью NULL-ссылки, и оно гарантирует, что вы всегда обрабатываете значения null. И если функция возвращает user, ее гарантируется как фактический экземпляр объекта. Awesomeness.

Теперь мы видим проблему в примере кода OP:

class Class { ... }

void main() {
    Class c = new Class(); // set to new Class() by default
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

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

Ответ 2

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

Исключение, показывающее, что вы забыли инициализировать c, скажет вам, в какой момент он не инициализирован, но не там, где он должен был быть инициализирован. Аналогично, пропущенный цикл (неявно) сообщает вам, где ему нужно иметь ненулевой .count, но не то, что должно было быть сделано или где. Я не вижу никого более легкого в программировании.

Я не думаю, что точка "no nulls" состоит в том, чтобы просто выполнять текстовую find-and-replace и превращать их в пустые экземпляры. Это явно бесполезно. Дело в том, чтобы структурировать ваш код, чтобы ваши переменные никогда не находились в состоянии, когда они указывают на бесполезные/неправильные значения, из которых NULL является просто наиболее распространенным.

Ответ 3

Я признаю, что я не очень много читал о SpeС#, но я понял, что NonNullable по сути является атрибутом, который вы добавляете в параметр, не обязательно в объявлении переменной; Превратите свой пример в нечто вроде:

class Class { ... }

void DoSomething(Class c)
{
    if (c == null) return;
    for(int i = 0; i < c.count; ++i) { ... }
}

void main() {
    Class c = nullptr;
    // ... ... ... code ...
    DoSomething(c);
}

С SpeС# вы отмечаете doSomething, чтобы сказать: "параметр c не может быть нулевым". Это кажется хорошей возможностью для меня, так как это означает, что мне не нужна первая строка в методе DoSomething() (который легко забыть и совершенно бессмыслен для контекста DoSomething()).

Ответ 4

Идея непустых типов заключается в том, чтобы позволить компилятору, а не вашему клиенту находить ошибки. Предположим, вы добавили на свой язык два спецификатора типа @nullable (может быть null) и @nonnull (никогда не null) (я использую синтаксис аннотации Java).

Когда вы определяете функцию, вы аннотируете ее аргументы. Например, следующий код будет компилировать

int f(@nullable Foo foo) {
  if (foo == null) 
    return 0;
  return foo.size();
}

Даже если foo может быть пустым в записи, поток управления гарантирует, что при вызове foo.size() foo является ненулевым.

Но если вы удалите чек для null, вы получите ошибку времени компиляции.

Следующее будет скомпилировано, потому что foo является ненулевым в записи:

int g(@nonnull Foo foo) {
  return foo.size(); // OK
}

Однако вы не сможете вызвать g с помощью указателя с нулевым значением:

@nullable Foo foo;
g(foo); // compiler error!

Компилятор выполняет анализ потока для каждой функции, поэтому он может обнаруживать, когда @nullable становится @nonnull (например, внутри оператора if, который проверяет значение null). Он также примет верификацию @nonnull при условии, что она будет немедленно инициализирована.

@nonnull Foo foo = new Foo();

В этой теме больше мой блог.

Ответ 5

Как я вижу, есть две области, где используется значение null.

Во-первых, это отсутствие значения. Например, логическое значение может быть истинным или ложным, или пользователь еще не выбрал параметр, следовательно, null. Это полезно и хорошо, но, возможно, было реализовано неправильно изначально, и теперь есть попытка формализовать это использование. (Должно ли быть второе логическое значение для сохранения состояния set/unset или null как часть логики с тремя состояниями?)

Второй - в смысле нулевого указателя. Это чаще всего проблема с программной ошибкой, т.е. исключение. Это не намеренное состояние, есть программная ошибка. Это должно быть под предлогом формальных исключений, реализованных на современных языках. То есть, NullException захватывается через блок try/catch.

Итак, какой из них вас интересует?

Ответ 6

В настоящее время я работаю над этой темой на С#..NET имеет Nullable для типов значений, но обратная функция не существует для ссылочных типов.

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

Ответ 7

Недействительные типы имеют больше смысла для меня, когда мы имеем дело с объектами домена. Когда вы сопоставляете таблицы базы данных с объектами, и у вас есть столбцы с нулевым значением. Скажем, у вас есть таблица под названием "Пользователь", и у нее есть столбец userid varchar (20), который не имеет значения NULL;

Было бы очень удобно иметь класс User с строковым полем UserId, который не может быть недействителен. Вы сокращаете количество ошибок во время компиляции.

Ответ 8

Мне нравится использование NULL. Это было некоторое время с тех пор, как я работал на С++, но мне очень легко было найти мои проблемы относительно возвращаемых значений и ссылок и исправить их.