Что такое ошибка кода null

Разбираемся в типах Kotlin — Unit, Nothing, Any (и null)

В Kotlin есть интересный набор базовых типов, который в какой-то мере соотносится с Java. Это Unit, Nothing, Any. Давайте посмотрим, что эти классы представляют собой, чем отличаются (несмотря на похожие названия), и сравним их с аналогами в Java. Ещё поговорим немного про null в Kotlin, потому что эти темы связаны.

Unit эквивалентен void в Java. В этом выражении возвращаемый тип можно не указывать, если функция ничего не возвращает. По умолчанию там будет Unit:

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

В стандартной библиотеке Kotlin Unit определён как объект, наследуемый от Any и содержащий единственный метод, переопределяющий toString():

Обратите внимание на ключевое слово object. Это значит, что Unit является синглтоном. Unit ничего не возвращает, а метод toString всегда будет возвращать “kotlin. Unit”. При компиляции в java-код Unit всегда будет превращаться в void.

Интересно, что в Java есть свой класс Void, который довольно слабо, но всё же соотносится с void и не может быть инстантирован. Это исключительно утилитарный класс, который нужен для рефлексии и дженериков в Java.

Nothing

С Nothing всë гораздо интереснее. Nothing — класс, который является наследником любого класса в Kotlin, даже класса с модификатором final. При этом Nothing нельзя создать — у него приватный конструктор. В коде он объявлен так:

Несмотря на всë это, класс Nothing довольно полезен. Так как невозможно передать или вернуть тип Nothing, он описывает результат «функции, которая никогда ничего не вернёт». Примером может быть функция, которая выбрасывает exception или в которой запущен бесконечный цикл: в любом из этих случаев она никогда не вернёт значения. В приложениях — независимо от того, какой тип данных возвращает функция, — она может никогда не вернуть данные, потому что произошла ошибка или вычисления затянулись на неопределённый срок. В этом случае имеет смысл использовать Nothing.

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

Вы можете наблюдать такую картину при автогенерации кода:

И хотя возвращаемое значение тут List<Data>, мы возвращаем Nothing. Именно потому что Nothing наследуется от всех классов:

Код прекрасно скомпилируется, потому что Nothing наследуется от Something. Но приложение сразу же упадёт с NotImplementedError, если вы вызовете метод doSomething.

Интересно, что в Java нельзя написать что-то подобное: код просто не скомпилируется, потому что Void не наследуется от String:

Ещё один пример может касаться выполнения, например, запроса данных из БД или удалённого сервера. Если произошла ошибка, можно возвращать null вместо данных. И это абсолютно нормально, данных-то нет:

А если хочется немного больше информации, чем просто null? Например, узнать тип ошибки. Вот тут Nothing приходит на помощь:

Вот как это может выглядеть в коде:

Класс Any находится на вершине иерархии — все классы в Kotlin являются наследниками Any. Any — это аналог Object в Java, но с меньшим количеством методов:

В java. lang. Object одиннадцать методов, и пять из них касаются многопоточности. Несмотря на меньшее количество методов, при компиляции в Java-код у любого класса появятся недостающие — тут можно быть спокойными.

В Java можно вызывать методы объекта всегда, и только в процессе работы программы вы узнаете, что объект у вас null. Тогда приложение падает с ошибкой NPE (NullPointerException). Это самая частая ошибка в Java вообще.

Создавая Kotlin, разработчики старались сделать его максимально безопасным. Напишем такой код:

Что получим в результате? Ошибку компиляции. Чтобы в переменной можно было хранить null, надо объявить её соответствующим образом:

Если попытаться присвоить переменной, не поддерживающей null, значение переменной с поддержкой nullable-типов, компилятор не даст скомпилировать такой код:

Также мы не сможем вызвать методы объекта, хранящегося в такой переменной:

Чтобы присвоить значение nullable-переменной, не поддерживающей null, надо сначала проверить, не содержит ли nullable-переменная null:

После этого в области видимости проверки переменная nullable будет рассматриваться компилятором как тип, не поддерживающий null. Можно безопасно обращаться к хранимому в переменной объекту.

Защита от NullPointerException реализована на уровне компилятора. Это касается не только переменных, но и выражений, и вызовов функций. Компилятор не даст передать в функцию значение переменной, которая поддерживает nullable-типы, если в определении функции заявлены только nonNull-параметры. И не даст присвоить переменной, не поддерживающей null, значение, полученное из выражения, которое может возвращать null.

Посмотрим ещё пример из Java:

При выполнении этого кода программа упадёт с ошибкой. И узнаем мы об этом только во время исполнения. Конечно, это упрощённый вариант кода, где ошибку видно сразу. Но в реальной программе обнаружить такие баги бывает непросто. Что предлагает Kotlin:

В Kotlin с его nullable-типами такой код попросту не скомпилируется. У класса Person в конструкторе заявлены параметры типов, не поддерживающих null в качестве значения. Поэтому компилятор может это отследить и не даст собрать такой код. Только если вы явно и с определённой целью укажете, что параметры могут быть null, — код скомпилируется и приложение запустится. Но тогда вся ответственность за NPE лежит на вас:

В Java нет поддержки null-безопасности на уровне языка, поэтому там приходится использовать аннотации. Это не только добавляет лишние строки кода, но и не гарантирует безопасность — код прекрасно собирается и принимает null даже там, где он не должен быть. Аннотация только подсвечивает проблемные места. В Kotlin же такой код просто не соберётся.

Оператор безопасного вызова

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

Производит проверку на null перед вызовом метода. Если значение переменной равно null, то вместо вызова исключения это выражение просто вернёт null. То есть если name == null, то nameLength будет == null или же nameLength будет == 4. Очень удобная и быстрая проверка, которая часто используется в коде (в своём приложении мы тоже будем прибегать к помощи этого оператора).

Оператор «Элвис»

Принцип работы следующий: если выражение слева вернёт не null, то мы получим длину имени; если же null, то вернётся значение выражения справа от оператора. Таким образом легко реализуется проверка на null и возврат значения по умолчанию. Исходя из примера выше, если name == null, то nameLength будет == 1 или же nameLength будет == 4.

На этом пока всё, остаёмся на связи! Это первая статья из цикла «Ликбез». Следующие материалы будут про коллекции, extensions, множественное наследование, Sealed Classes и многое другое — следите за блогом!

Учимся избегать null-значений в современном Java. Часть 1

Java

В своем выступлении “Null References: The billion dollar mistake” (“Нулевые ссылки: ошибка на миллиард долларов”), Тони Хоар описывает реализацию нулевых ссылок в языках программирования ALGOL, что также по его словам стало ошибкой стоимостью в миллиард долларов. Такие авторитетные книги, как Clean Code: A Handbook of Agile Software Craftsmanship (“Чистый код: настольное руководство по гибкой разработке ПО”) рекомендуют использовать нуль как можно реже. В то же время в книге Bug Patterns in Java (“Шаблоны ошибок в Java”) проблемам, связанным с нулевыми значениями, посвящается аж целых три главы. Тема “What is a null pointer exception and how do I fix it” (“Что такое исключение нулевого значения и как его исправить”), обсуждаемая на Stack Overflow, набрала уже более 3 млн просмотров. Работа с нулевыми значениями и впрямь может вызвать немало сложностей.

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

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

Чем опасен null?

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

В результате любое возвращаемое значение или объект параметра — это потенциальное исключение нулевого указателя (NPE), возникающее в случае неправильной обработки.

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

Может тогда вообще никогда не присваивать значениям null? Тоже неудачное предположение. Если учесть тот факт, что в каждом языке программирования есть пустое значение ( nil, undefined, None, void и т. д.), то наличие общего значения, обозначающего отсутствие чего-либо, чрезвычайно полезно.

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

Когда Null уместен, а когда нет

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

Возвращение null

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

Сейчас я работаю над проектом электронной записи пациентов Columna, в котором у нас есть классы, представляющие элементы рабочего процесса больницы вроде пациентов, медикаментов, врачей, больничных отделений, госпитализаций и пр. При моделировании любой области возникают случаи, когда нам нужно допустить для определенного элемента отсутствие значения. Предположим, что у нас есть класс, представляющий госпитализацию с атрибутами, которые ее описывают: больничное отделение, куда помещается пациент, причина госпитализации, ее время и т. д. Аналогичным образом у нас может быть класс, который представляет пациента с набором атрибутов вроде имени и номера социального страхования. В любой момент времени пациент может быть госпитализирован или нет. Говоря более формально, у нас есть связь типа “имеет” с мощностью 0..1.

Представим метод, извлекающий из базы данных информацию о госпитализации данного пациента:

Что должен возвращать этот метод для не госпитализированного пациента, если не null? Есть ли для выражения этого более точное значение? Спорю, что нет.

Существует и много других сценариев, в которых объект области напрямую ассоциирован с необязательными значениями в качестве атрибутов. Объект, представляющий медикаменты, содержит такие значения, как название препарата, его форму, активность, действующее вещество и т. д. Однако разнообразие медикаментов чрезвычайно обширно, начиная от антибактериальных кремов и заканчивая чаями из каннабиса (оставим или на ромашку заменим?:)), следовательно не все атрибуты будут актуальны для каждого. И снова возвращение null для отсутствующего атрибута выглядит очевидным способом сообщить, что допускается отсутствие значения. Есть ли для этого лучшая альтернатива?

Некоторые выступают за использование так называемого шаблона проектирования Null Object вместо null. Главная его идея в реализации пустого класса с минимумом или вообще без функциональности, т. е. нулевого объекта, который можно использовать вместо класса, содержащего действительную функциональность. Лучше всего данный шаблон показывает себя в сценариях, где нужно рекурсивно обойти структуру бинарного дерева в поиске суммы значений всех узлов. Для этого вы выполняете поиск в глубину и рекурсивно суммируете в каждом узле значения его левого и правого поддерева.

Дерево поиска обычно реализуется так, что каждый узел в нем имеет левого и правого потомка, которые являются либо также узлами, либо концевыми вершинами. Если такое представление дерева использует для концевых узлов null, то вам придется явно выполнять проверки на нулевых потомков, чтобы останавливать рекурсию в концевом узле, предотвращая попытку получения его значения. Вместо этого вам следует определить интерфейс Node с простым методом getValue() и реализовать его в представляющем узел классе, который вычисляет значение, складывая значения getValues() потомков, как показано на рисунке ниже. Реализуйте такой же интерфейс в классе, представляющем узел, и пусть класс концевого узла возвращает при вызове 0. Теперь нам больше не нужно различать код между концевым узлом и обычным. Необходимость явно проверять наличие null отпала вместе с риском получения исключения (NPE).

Применение шаблона нулевого объекта к структуре дерева

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

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

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

Передача нулевых параметров

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

Как же полностью избежать нулевых параметров?

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

В таких языках, как Python, сигнатуры методов могут содержать предустановленные значения параметров, используемые при отсутствии значения аргумента в вызове метода. Тем не менее в Java такое невозможно. Ближайшим аналогом этого будет использовать перегрузку метода, когда в классе одна и та же сигнатура метода определяется несколько раз с разными параметрами. Один метод будет содержать всю функциональность и принимать весь набор параметров, а другие будут просто “декораторами” для вызова этого метода, каждый из которых будет получать свой поднабор параметров. Методы-декораторы определяют, какие значения должны использовать вместо отсутствующих параметров, чтобы вызывающему компоненту не пришлось их предоставлять. Жестко прописывая, какие значения должны предоставляться, когда у вызывающего их не хватает, мы уменьшаем риск появления ошибок и делаем принимаемые значения параметров явными.

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

Воспринимайте null правильно

Все больше языков программирования начинают реализовывать определенные возможности с учетом безопасности. Например, в таких языках, как Clojure, F# и Rust переменные по умолчанию неизменяемы. Компилятор допускает изменение значений только для тех из них, которые объявлены со специальным модификатором. Такой способ использования опасных функций вынуждает программистов переопределять поведение по умолчанию, указывая тем самым, что они осознают степень риска и делают это не без весомых оснований. И к null нам стоит относиться аналогичным образом. Нужно придерживать это значение для особых случаев, где оно будет вполне уместно, ограничив при этом его использование в целом, опять же не ценой усложнения кода креативными обходными решениями. При каждом намерении использовать null вместо перемещающегося между методами значения следует учесть оправданность этого. В таком случае вы должны гарантировать, что в итоге оно не окажется в том месте, где может вызвать проблемы, и другие разработчики будут знать, что значение может быть null. Если же этого обеспечить нельзя, то лучше рассмотреть другие варианты.

Источники:

https://gb. ru/posts/razbiraemsya-v-tipah-kotlin-unit-nothing-any-i-null

https://nuancesprog. ru/p/10004/

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: