что это такое и как его использовать

1656600045 chto eto takoe i kak ego ispolzovat

Кристиана Ноймана

Какое значение null? Как null реализовано? Когда следует использовать null в исходном коде и когда вы должны нет используй это?

SlPlSzIxTpuDJvmK9d1gh7crctKc0PGxjkNT

Введение

null является основополагающим понятием во многих языках программирования. Он распространен во всех видах исходного кода, написанного на этих языках. Поэтому важно полностью понять идею null . Мы должны понимать его семантику и реализацию, а также знать, как использовать null в нашем исходном коде.

Комментарии на форумах программистов иногда обнаруживают небольшую путаницу null. Некоторые программисты даже пытаются полностью избегать null. Потому что они считают это «ошибкой на миллион долларов», термин, введенный Тони Хоаром, изобретателем null.

Вот простой пример: предположим, что Алиса email_address указывает на null. Что это значит? Значит ли это, что у Алисы нет электронного адреса? Или что ее электронный адрес неизвестен? Или что это тайно? Или это просто означает это email_address «неопределенный» или «неинициализированный»? Давайте посмотрим. Прочитав статью, каждый сможет без колебаний ответить на такие вопросы.

Примечание: Эта статья нейтральна по языку программирования – насколько это возможно. Объяснения общие и не привязаны к конкретному языку. Пожалуйста, обратитесь к руководству по языку программирования, чтобы получить конкретные советы null. Однако эта статья содержит несколько простых примеров исходного кода, показанных на Java. Но перевести их на любимый язык не сложно.

Реализация во время выполнения

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

Примечание: Мы посмотрим на а типичный внедрение null. Фактическая реализация в данной среде зависит от языка программирования и целевой среды и может отличаться от реализации, показанной здесь.

Предположим, у нас есть следующая инструкция исходного кода:

String name = "Bob";

Здесь мы объявляем переменную типа String и с идентификатором name что указывает на строку "Bob".

В этом контексте важно сказать «указывает на», поскольку мы предполагаем, что работаем справочные типы (а не из типы ценностей). Подробнее об этом позже.

Чтобы все было просто, мы сделаем следующие догадки:

  • Вышеприведенная инструкция выполняется на 16-разрядном ЦБ с 16-битным адресным пространством.
  • Строки кодируются как UTF-16. Они оканчиваются 0 (как у C или C++).

На следующем рисунке показан отрывок памяти после выполнения приведенной выше инструкции:

eTdrWFVeUC11ONGB8xHgStH5N55hjuDjemAe
Рисунок 1: Переменная name указывает на «Боб»

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

Как видим, строчка "Bob" хранится по адресу B000 и занимает 4 ячейки памяти.

Переменная name находится по адресу A0A1. Содержимое A0A1 – это B000, являющееся начальным местом памяти строки "Bob". Поэтому мы говорим: переменная name указывает на "Bob".

Всё идет нормально.

Теперь допустим, что после выполнения приведенной выше инструкции вы выполняете следующее:

name = null;

Теперь name указывает на null.

И это новое состояние в памяти:

sJgbydICZG7fnaM3o8qYN0jicPPoeZmDzMqR
Рисунок 2: Переменная name указывает на null

Мы видим, что для строчки ничего не изменилось "Bob" который до сих пор хранится в памяти.

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

Важно, чтобы содержимое A0A1 (представляющее значение переменной name) теперь 0000. Итак, переменная name не указывает на "Bob" больше. Значение 0 (все биты на нуле) является типичным значением, используемым в памяти для обозначения null. Это означает, что есть не связанное значение name. Вы также можете думать об этом как отсутствие данных или просто нет данных.

Примечание. Фактическое значение памяти, используемое для обозначения null зависит от реализации. К примеру, спецификация виртуальной машины Java указывается в конце раздела 2.4. “Справочные типы и значения:»

Спецификация виртуальной машины Java не требует кодирования конкретного значения null.

Помните:

Если ссылка указывает на nullэто просто означает, что есть никакой ценности, связанной с этим.

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

Производительность

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

Существует только два вида операций:

  • Инициализируйте или установите ссылку на null (например name = null): Единственное, что нужно сделать, это изменить содержимое одной ячейки памяти (например, установить ее на 0).
  • Проверьте, указывает ли ссылка на null (например if name == null): Единственное, что нужно сделать, это проверить, содержит ли ячейка памяти ссылку 0.

Помните:

Операции на null очень быстрые и дешевые.

Ссылки против типов значений

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

Почему?

Как мы видели ранее, ссылкой является a указатель к адресу памяти, который сохраняет значение (например, строка, дата, что угодно). Если ссылка указывает на nullто значение не связано с ним.

Не считая того, ценность — это по определению сама цена. Нет указателя. Тип значения сохраняется как самое значение. Поэтому концепция null не существует для типов значений.

Следующее изображение показывает разницу. С левой стороны вы можете снова увидеть память в случае переменной name является ссылкой, указывающей на «Боб». Справа показывает память в случае переменной name являясь типом значения.

lp5yoXOXWz72BIqJwzRFOqtIbcvckaixOIQg

Как видим, в случае типа значения само значение непосредственно сохраняется по адресу A0A1, который связан с переменным name.

Можно было бы многое сказать о ссылках и типах значений, но это выходит за рамки этой статьи. Обратите внимание, что некоторые языки программирования поддерживают только ссылки на типы, другие поддерживают только типы значений, а некоторые (например, C# и Java) поддерживают оба.

Помните:

Концепция null существует только для ссылка типы. Оно не существует для типы ценностей.

Смысл

Предположим, у нас есть тип person с полем emailAddress. Предположим также, что для данного человека, которого мы назовем Алисой, emailAddress указывает на null.

Что это значит? Значит ли это, что у Алисы нет электронного адреса? Не обязательно.

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

Но почему нет ли ценности? В чем причина emailAddress указывая на null? Если мы не знаем контекст и историю, то можем только догадываться. Причина для null мог быть:

У Алисы нет электронного адреса. Или…

Алиса имеет адрес электронной почты, но:

  • он еще не внесен в базу данных
  • это тайно (не раскрывается из соображений безопасности)
  • есть ошибка в подпрограмме, которая создает объект person без установки поля emailAddress
  • и так дальше.

На практике мы часто знаем применение и контекст. Мы интуитивно связываем точное значение с null. В простом и безупречном мире, null это просто означает, что у Алисы на самом деле нет электронного адреса.

Когда мы пишем код, причина почему ссылка указывает на null часто не имеет значения. Мы просто проверяем null и принять соответствующие действия. К примеру, предположим, что нам нужно написать цикл, который посылает электронные письма для списка лиц. Код (на Java) может выглядеть так:

for ( Person person: persons ) {    if ( person.getEmailAddress() != null ) {        // code to send email    } else {        logger.warning("No email address for " + person.getName());    }}

В приведенном выше цикле нас не волнует причина null. Мы просто признаем тот факт, что нет адреса электронной почты, регистрируем предупреждение и продолжаем.

Помните:

Если ссылка указывает на null тогда это всегда означает, что есть никакой ценности, связанной с этим.

В большинстве случаев, null имеет более конкретное значение, которое зависит от контекста.

Почему это null?

Иногда это есть важно знать почему ссылка указывает на null.

Рассмотрим следующую сигнатуру функции в медицинской программе:

List<Allergy> getAllergiesOfPatient ( String patientId )

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

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

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

НЕ ДЕЛАЙТЕ ЭТОГО!

Это плохой дизайн данных по нескольким причинам.

Разная семантика возвращения null по сравнению с возвратом пустого списка нужно было бы хорошо задокументировать. И, как мы все знаем, комментарии могут быть неправильными (т.е. несовместимыми с кодом), устаревшими или даже недоступными.

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

List<Allergy> allergies = getAllergiesOfPatient ( "123" );				if ( allergies == null ) {    System.out.println ( "No allergies" );             // <-- WRONG!} else if ( allergies.isEmpty() ) {    System.out.println ( "Test not done yet" );        // <-- WRONG!} else {    System.out.println ( "There are allergies" );}

Следующий код также будет неправильным:

List<Allergy> allergies = getAllergiesOfPatient ( "123" );if ( allergies == null || allergies.isEmpty() ) {    System.out.println ( "No allergies" );             // <-- WRONG!} else {    System.out.println ( "There are allergies" );}

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

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

Таким образом, функция должна возвращать больше информации, чем просто список.

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

Чтобы различить случаи, мы определяем родительский тип AllergyTestResultа также три подтипа, представляющих три случая (NotDone, Pendingи Done):

interface AllergyTestResult {}
interface NotDoneAllergyTestResult extends AllergyTestResult {}
interface PendingAllergyTestResult extends AllergyTestResult {    public Date getDateStarted();}
interface DoneAllergyTestResult extends AllergyTestResult {    public Date getDateDone();    public List<Allergy> getAllergies(); // null if no allergies                                         // non-empty if there are                                         // allergies}

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

Вместо того чтобы просто возвращать список, getAllergiesOfPatient теперь возвращает an AllergyTestResult объект:

AllergyTestResult getAllergiesOfPatient ( String patientId )

Клиентский код теперь менее подвержен ошибкам и выглядит так:

AllergyTestResult allergyTestResult = getAllergiesOfPatient("123");
if (allergyTestResult instanceof NotDoneAllergyTestResult) {    System.out.println ( "Test not done yet" );   } else if (allergyTestResult instanceof PendingAllergyTestResult) {    System.out.println ( "Test pending" );   } else if (allergyTestResult instanceof DoneAllergyTestResult) {    List<Allergy> list = ((DoneAllergyTestResult)         allergyTestResult).getAllergies();    if (list == null) {        System.out.println ( "No allergies" );    } else if (list.isEmpty()) {        assert false;    } else {        System.out.println ( "There are allergies" );    }} else {    assert false;}

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

Помните:

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

Инициализация

Обратите внимание на следующие инструкции:

String s1 = "foo";String s2 = null;String s3;

Первая инструкция объявляет a String переменный s1 и присваивает ему значение "foo".

Вторая инструкция назначает null к s2.

Самой интересной инструкцией является последняя. Значение не назначается явно s3. Поэтому разумно спросить: какое состояние s3 после его декларирования? Что будет, если мы напишем s3 на устройство вывода ОС?

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

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

  • Незаконно объявлять переменную без присвоения значения
  • В нем сохраняется произвольное значение s3в зависимости от объема памяти на момент выполнения – значения по умолчанию нет
  • Значение по умолчанию назначается автоматически s3. В случае типа ссылки значение по умолчанию есть null. В случае типа значения значение по умолчанию зависит от типа переменной. Например 0 для целых чисел, false для логического значения и т.д.
  • состояние s3 является «неопределенным»
  • состояние s3 является «неинициализированным», и любые попытки использования s3 приводит к ошибке при компиляции.

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

В качестве примера Java применяет последнюю опцию для локальных переменных. Следующий код приводит к ошибке во время компиляции во второй строке:

String s3;System.out.println ( s3 );

Выход компилятора:

error: variable s3 might not have been initialized

Помните:

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

В некоторых языках, null является значением по умолчанию для типов ссылок.

Когда использовать null (И когда не использовать)

Основное правило простое: null должно быть разрешено только тогда, когда имеет смысл, чтобы ссылка на объект не имела связанного с ним значения. (Примечание: ссылкой на объект может быть переменная, константа, свойство (поле класса), аргумент ввода/вывода и т.д.)

Например, допустим тип person с полями name и dateOfFirstMarriage:

interface Person {    public String getName();    public Date getDateOfFirstMarriage();}

У каждого человека есть имя. Поэтому это не имеет смысла для поля name иметь «не связанное с ним значение». Поле name есть не имеет значения NULL. Назначение запрещено null к нему.

С другой стороны, поле dateOfFirstMarriage не представляет требуемое значение. Не все женаты. Поэтому это имеет смысл для dateOfFirstMarriage иметь «не связанное с ним значение». Поэтому dateOfFirstMarriage это обнуляется поле. Если у человека dateOfFirstMarriage поле указывает на null то это просто означает, что это лицо никогда не было замужем.

Примечание: К сожалению, большинство популярных языков программирования не различают типы, имеющие значение NULL, и не имеющие значения. Нет способа достоверно это утверждать null никогда не может быть предназначен для данной ссылки на объект. В некоторых языках можно использовать аннотации, например, нестандартные аннотации @Nullable и @NonNullable в Java. Вот пример:

interface Person {    public @Nonnull String getName();    public @Nullable Date getDateOfFirstMarriage();}

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

Важно отметить, что null не следует использовать для обозначения условий ошибки.

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

public Config readConfigFromFile ( File file )

Что должно произойти в случае ошибки чтения файла?

Просто вернуть null?

НЕТ!

Каждый язык имеет свой собственный стандартный способ сигнализации об условиях ошибки и предоставления данных об ошибке, таких как описание, тип, трассировка стека и т.д. Многие языки (C#, Java и т.д.) используют механизм исключений, и в этих языках следует использовать исключения для сигнала об ошибках во время выполнения. readConfigFromFile не должен возвращаться null для обозначения ошибки. Вместо этого следует изменить подпись функции, чтобы было ясно, что функция может выйти из строя:

public Config readConfigFromFile ( File file ) throws IOException

Помните:

Разрешить null только в том случае, если ссылка на объект имеет смысл «не иметь связанного с ним значения».

Не используйте null сигнализировать об ошибках

Ноль-безопасность

Рассмотрим следующий код:

String name = null;int l = name.length();

При выполнении вышеприведенный код приводит к печально известному ошибка нулевого указателяпотому что мы стараемся выполнить метод ссылки, на который указывает null. В C#, например, a NullReferenceException бросается, в Java это a NullPointerException.

Ошибка нулевого указателя неприятна.

Это самая частая ошибка во многих программных приложениях, и являлся причиной бесчисленных проблем в истории разработки программного обеспечения. Тони Хоар, изобретатель nullназывает это «ошибкой в ​​миллиард долларов».

Но Тони Хоар (лауреат премии Тьюринга в 1980 году и изобретатель алгоритма быстрой сортировки) в своей речи также дает намек на решение:

Более современные языки … ввели объявления для ненулевых ссылок. Это решение, которое я отклонил в 1965 году.

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

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

Null-safety – это увлекательная тема, которая заслуживает отдельной статьи.

Помните:

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

Примечание. Некоторые языки программирования (преимущественно функциональные языки программирования, такие как Haskell) не поддерживают концепцию null. Вместо этого они используют Возможно/Необязательный шаблон представлять «отсутствие ценности». Компилятор гарантирует, что случай без значения обрабатывается явно. Следовательно, ошибки нулевого указателя не могут возникать.

Резюме

Вот краткое изложение ключевых моментов, которые следует помнить:

  • Если ссылка указывает на nullэто всегда означает, что есть никакой ценности, связанной с этим.
  • В большинстве случаев, null имеет более конкретное значение, зависящее от контекста.
  • Если нам нужно знать почему не имеет значения, связанного со ссылкой, тогда необходимо предоставить дополнительные данные, чтобы различать возможные случаи.
  • Разрешить null только в том случае, если ссылка на объект имеет смысл «не иметь связанного с ним значения».
  • Не используйте null сигнализировать об ошибках
  • Концепция null существует только для справочных типов. Он не существует для типов значений.
  • В некоторых языках null является значением по умолчанию для типов ссылок.
  • null операции очень быстрые и дешевые.
  • По возможности используйте язык, поддерживающий компиляцию-время-ноль-безопасность.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *