Лучшее объяснение для языков без null


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

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

  • нежелательность наличия ссылки / указатели могут быть обнулены по умолчанию
  • как работают типы опций, включая стратегии для облегчения проверки нулевых случаев, таких как
    • сопоставление с образцом и
    • монадическом пониманий
  • альтернативное решение, такое как сообщение ест ноль
  • (другие аспекты я упустил)
11 215

11 ответов:

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

предположим, я моделирую дверь. Он может находиться в одном из трех состояний: открыт, закрыт, но заперта, а заперла. Теперь я мог бы моделировать его по линиям

class Door
    private bool isShut
    private bool isLocked

и понятно, как сопоставить мои три состояния в эти две булевы переменные. Но это оставляет четвертое, нежелательное состояние доступным:isShut==false && isLocked==true. Потому что типы, которые я выбрал в качестве своего представления, допускают это состояние, я должен потратить умственные усилия, чтобы гарантировать, что класс никогда не попадет в это состояние (возможно, явно кодируя инвариант). Напротив, если бы я использовал язык с алгебраическими типами данных или проверенными перечислениями, который позволяет мне определить

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

тогда я мог бы определить

class Door
    private DoorState state

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

проблема с null это то, что каждый ссылочный тип получает это дополнительное состояние в своем пространстве, которое обычно нежелательно. А string переменная может быть любой последовательностью символов, или это может быть этот сумасшедший экстра null значение, которое не отображается в моей проблемной области. А система типов, изменение от "nullable references по умолчанию" до " non-nullable references by default " - это почти всегда простое изменение, которое делает систему типов намного лучше в борьбе со сложностью и исключении определенных типов ошибок и бессмысленных состояний. Так что это довольно безумно, что так много языков продолжают повторять эту ошибку снова и снова.)

хорошая вещь о типах опций заключается не в том, что они необязательны. Он заключается в том, что все остальные типы не.

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

но часто, нам это не нужно, и позволяет такое "нулевое" состояние приводит только к двусмысленности и путанице: каждый раз, когда я обращаюсь к переменной ссылочного типа в .NET, я должен учитывать, что это может быть null.

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

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

Это сложно достичь в .NET, где почти все может быть null. Вы должны полагаться на автора кода, который вы вызываете, чтобы быть на 100% дисциплинированным и последовательным и четко документировать, что может и не может быть null, или вы должны быть параноиком и проверять все.

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

и тогда нам просто нужна задняя дверь для редких случаев, где мы do нужно обрабатывать нулевое состояние. Затем можно использовать тип "option". Затем мы допускаем null в тех случаях, когда мы приняли сознательное решение, что нам нужно иметь возможность представлять случай "без значения", и в каждом другом случае мы знаем, что значение никогда не будет null.

Как другие упоминали, например, в C# или Java, null может означать одну из двух вещей:

  1. переменная не инициализирована. Это должно, в идеале, никогда произойдет. Переменная не должна если он не инициализируется.
  2. переменная содержит некоторые "необязательные" данные: она должна быть в состоянии представить случай, когда нет данных. Это иногда необходимо. Возможно, вы пытаетесь найти объект в списке, и вы не знаю заранее, есть ли он там. Тогда мы должны быть в состоянии представить, что "объект не найден".

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

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

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

это все хорошие вещи! Но это не исключает использования явно обнуляемых / ненулевых типов для достижения того же эффекта. Почему же тогда опцион все еще хорош? В конце концов, Scala поддерживает значения nullable (is и to, поэтому он может работать с библиотеками Java), но поддерживает Options как хорошо.

вопрос: Итак, каковы преимущества за пределами возможности полностью удалить нули из языка?

А. Композиция

если вы делаете наивный перевод с нулевого кода

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

к коду с поддержкой опций

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

большой разницы нет! Но это еще и Грозный способ использования опционов... Этот подход намного чище:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

или еще:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

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

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

как это работает?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

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

так как люди, кажется, не хватает его:null неоднозначно.

Дата рождения Алисы null. Что это значит?

дата смерти Боба -null. Что это значит?


еще одна проблема:null ребро случай.

  • и null = null?
  • и nan = nan?
  • и inf = inf?
  • и +0 = -0?
  • и +0/0 = -0/0?

ответы обычно" да"," нет"," да"," да"," нет"," да " соответственно. Сумасшедшие " математики "называют NaN" ничтожеством " и говорят, что он сравнивает себя с самим собой. SQL рассматривает нули как не равные ничему (поэтому они ведут себя как NaNs). Интересно, что происходит, когда вы пытаетесь сохранить ±∞, ±0, и NaNs в том же столбце базы данных (есть 253 NaNs, половина из которых"отрицательные").

что еще хуже, базы данных отличаются тем, как они обрабатывают NULL, и большинство из них не согласованы (см. обработка NULL в SQLite обзор). Это довольно ужасно.


а теперь об обязательном рассказе:

недавно я разработал таблицу базы данных (sqlite3) с пятью столбцами a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Потому что это общий схема предназначена для решения общей проблемы для довольно произвольных приложений, есть два ограничения уникальности:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_a существует только для совместимости с существующим дизайном приложения (отчасти потому, что я не придумал лучшего решения) и не используется в новом приложении. Из-за того, как NULL работает в SQL, я могу вставить (1, 2, NULL, 3, t) и (1, 2, NULL, 4, t) и не нарушать первое ограничение уникальности (потому что (1, 2, NULL) != (1, 2, NULL)).

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


FWIW, без первого вызова неопределенного поведения, ссылки C++ не могут "указывать" на null, и невозможно построить класс с неинициализированными ссылочными переменными-членами (если возникает исключение, построение завершается неудачей).

заметка на полях: Иногда вам могут понадобиться взаимоисключающие указатели (т. е. только один из них может быть ненулевым), например, в гипотетической iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Вместо этого, я вынужден делать такие вещи, как assert((bool)actionSheet + (bool)alertView == 1).

нежелательность наличия ссылок / указателей по умолчанию обнуляется.

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

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

языки, которые поддерживают типы опций, как правило, также запрещают или препятствуют использованию неинициализированных переменных.

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

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

В F#:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

однако, в таком языке, как Java без прямой поддержки типов опций, у нас было бы что-то вроде:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

альтернативное решение, такое как сообщение ест ноль

Objective-C "message eating nil" -это не столько решение, сколько попытка облегчить головную боль от нулевой проверки. В принципе, вместо того, чтобы создавать исключение во время выполнения при попытке вызвать метод для объекта null, выражение вместо этого само вычисляется как null. Приостановив недоверие, это как если бы каждый метод экземпляра начинался с if (this == null) return null;. Но тогда есть потеря информации: вы не знаете, вернул ли метод null, потому что это допустимое возвращаемое значение, или потому что объект на самом деле null. Это очень похоже на проглатывание исключений и не делает никакого прогресса в решении проблем с нулевым выделением до.

сборка принесла нам адреса также известные как нетипизированные указатели. C сопоставил их непосредственно как типизированные указатели, но ввел значение null Algol как уникальное значение указателя, совместимое со всеми типизированными указателями. Большая проблема с null в C заключается в том, что, поскольку каждый указатель может быть null, никто не может использовать указатель без ручной проверки.

в языках более высокого уровня наличие null неудобно, поскольку оно действительно передает два разных понятия:

  • говоря, что что-то есть неопределено.
  • сказать, что что-то есть дополнительно.

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

второй случай-это опциональность и лучше всего предоставляется явно, например, с параметр типа.


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

В C мы могли бы иметь:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

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

в OCaml тот же код будет выглядеть так:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

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

В C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

в OCaml это будет:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

как вы можете видеть в этом тривиальном примере, в безопасной версии нет ничего сложного:

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

в то время как в C вы могли просто забыть нулевую проверку и бум...

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

Microsoft Research имеет интересный проект под названием

Spec#

Это расширение C# с Не-нулевого типа и какой-то механизм, чтобы Проверьте свои объекты против того, чтобы не быть null, хотя, ИМХО, применяя оформление по договору принцип может быть более подходящим и более полезным для многих проблемных ситуаций, вызванных пустыми ссылками.

Роберт Нистром предлагает хорошую статью здесь:

http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/

описание его мыслительного процесса при добавлении поддержки отсутствия и неудачи к его Сорочьи язык программирования.

исходя из .NET фона, я всегда думал, что null имеет смысл, его полезно. Пока я не узнал о структурах и о том, как легко с ними работать, избегая большого количества шаблонного кода. Тони Хоара выступая на QCon London в 2009 году,извинился за изобретение пустая ссылка. Процитирую его:

Я называю это моей миллиардной ошибкой. Это было изобретение null справка за 1965 год. В то время я проектировал первый комплексная система типов для ссылок в объектно-ориентированном виде язык (ALGOL W). Моя цель состояла в том, чтобы обеспечить все использование ссылок должно быть абсолютно безопасно, с проверкой выполненной автоматически мимо компилятор. Но я не мог устоять перед искушением поставить в нуль ссылка, просто потому, что это было так легко осуществить. Это привело к бесчисленные ошибки, уязвимости и сбои системы, которые имеют вероятно, причинил миллиард долларов боли и ущерба за последние сорок лет. годы. В последние годы, несколько анализаторов программы любят префикс и PREfast в Microsoft были использованы для проверки ссылок, а также дать предупреждения при наличии риска они могут быть ненулевыми. Более свежий языки программирования, такие как Spec#, ввели объявления для ненулевые ссылки. Это решение, которое я отверг в 1965 году.

посмотреть этот вопрос тоже на программистов

Я всегда смотрел на Null (или nil) как на отсутствие значение.

иногда хочется, иногда нет. Это зависит от домена, который вы работаете. Если отсутствие имеет смысл: нет второго имени, то ваше приложение может действовать соответственно. С другой стороны, если значение null не должно быть там: первое имя null, то разработчик получает пресловутый 2-часовой телефонный звонок.

Я также видел перегруженный код и слишком сложно с проверками на null. Для меня это означает одно из двух:
a) ошибка выше в дереве приложений
b) плохой / неполный дизайн

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

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

векторные языки иногда могут сойти с рук, не имея null.

пустой вектор служит типизированный null в этом случае.