- Nullable-типы в Kotlin
- Kotlin и работа с NULL
- Защита от null, ключевое слово let
- Элвис-оператор ?:
- Оператор !!
- Ключевое слово let
- Ключевые слова и операторы
- Жесткие ключевые слова (Hard Keywords)
- Мягкие Ключевые Слова (Soft Keywords)
- Модификаторы (Modifier Keywords)
- Специальные идентификаторы (Special Identifiers)
- Операторы и специальные символы (Operators and Special Symbols)
Nullable-типы в Kotlin
В Kotlin и других языках программирования есть такое значение как null (или подобное ему). Это литерал, который по идее можно присвоить переменной любого типа, чтобы явно указать, что ни с какими данными она не связана.
Однако, если потом вызвать функцию, которая не ожидает null, а предполагает, что ей всегда будут передавать вполне себе данные, возникнет ошибка. Например, если строковая переменная связана не со строкой, а с null, то узнать ее длину через ее свойство length нельзя, это бессмысленно. Но поскольку такое свойство у строк есть, и мы можем его вызвать, то подобное действие, если оно происходит в процессе выполнения программы, приведет к тому, что программа аварийно завершит свою работу.
Kotlin, в отличии от той же Java, устроен таким образом, что если переменная потенциально может содержать null, к ней нельзя применять функции, которые не обрабатывают такие случаи, не предполагают принятия null. Для этого в Kotlin разделяют типы данных на две группы:
те, которые не поддерживают null, и
так называемые nullable-типы, которым среди прочего можно присвоить null.
Nullable можно перевести как обнуляемые. Другими словами, это типы с поддержкой пустого значения. Подобное разделение помогает избегать ошибок во время выполнения программы, так как то, что ошибка потенциально возможна, становится ясно на этапе написания программы. И компилятор Kotlin не даст скомпилировать такой потенциально-ошибочный код.
Зачем нужен null, если переменную в языках со статической типизацией можно просто не инициировать значением (не определять ее), а только объявить? Такая переменная ведь тоже ни на что не указывает. Однако неинициированную переменную нельзя использовать в выражениях, а бывают случаи, когда надо, даже если данных может и не быть.
Так, например, функция readLine() читает строку (в смысле линию текста) со стандартного ввода. По умолчанию на стандартный ввод поступают данные с клавиатуры. Однако можно перенаправить поток ввода на чтение из файла. В этом случае readLine() будет читать файл строку за строкой, то есть линию за линией. Когда функция попытается прочитать что-то после последней строки, где уже ничего нет, то вернет null. Программе этот сигнал сообщит, что достигнут конец файла.
Таким образом, даже если мы читаем данные с клавиатуры, функция readLine() предполагает возможный возврат из себя null. Хотя при чтении из клавиатурного потока ввода получить null почти невозможно, так как даже пустая строка – это не null, а обычная строка с нулевым количеством символов – «» . Несмотря на это, в Kotlin мы не может присвоить результат, возвращенный readLine(), переменной типа String. Когда раньше мы писали так val s = readLine() , то на самом деле тип переменной был не String, а немного другим – String?.
Разница между типами String и String? только в том, что последний допускает значение null. Переменным этого типа можно присвоить такое значение. Переменным, чей тип не имеет вопросительного знака в конце имени типа, присваивать null в Kotlin запрещено. Аналогичные nullable-вариации есть у других типов. Так типу Int соответствует Int?, типу Double – Double? и т. д.
Сравним в работе обычную строку и строку с возможным null. Измерить длину строки типа String? не удастся, потому что свойство length работает только с данными типа String.
Что делать, если readLine() возвращает String? , однако мы знаем, что null там быть не может, и хотим узнать длину строки, то есть количество символов в ней? На помощь может придти условный оператор. Если переменная указывает не на null, то измерить длину. Если на null, то сделать что-то еще. Например, присвоить переменной, предназначенной для хранения длины, значение -1. (Примечание: оператор != означает «не равно».)
Посмотрите внимательно на программу. Переменная s1 нигде явно не меняет своего типа, она так и остается nullable. Как же мы измеряем ее длину в выражении s1.length ? По-идее в теле ветки if мы должны были вручную привести ее к обычному строковому типу командой s1.toString() . При этом создается копия значения s1 , но уже типа String. Она может быть присвоена другой переменной. И только к этому новому String-значению мы могли применять свойство length.
На самом деле примерно так и происходит. Умный компилятор Kotlin делает рутинную работу за нас. Подобное явление называется умным приведением типов (smart cast). Когда в теле if мы пытаемся измерить длину, то компилятор за нас приводит строку, содержащуюся в s1 , к типу String, потому что из условия уже известно, что это не null, и только после этого вызывает length.
Если все-таки присвоить значение null переменной типа String? , то сработает ветка else, никакого приведения типа не будет, а в переменную lenS запишется значение -1, которое в нашем коде мы можем интерпретировать как сигнал об отсутствии строки.
Конструкция с if-else слишком громоздкая для выполнения такой мелкой задачи, как обработка значения null. Мы можем записать ее в одну строку:
Здесь всего-лишь убрали переходы на новую строку и отступы, которые нам были нужны для читаемости кода. Компилятору они безразличны.
Однако в Kotlin существуют другие, более короткие варианты работы с nullable-типами.
Если после переменной поставить вопросительный знак, после которого вызвать функцию (или свойство), не принимающую null, то эта функция будет вызвана, только если значение не null. Если же оно null, то функция вызвана не будет. Будет возвращен null.
В примере выражение s?.length возвращает null, потому что s связана с null. В иных случаях вернулась бы длина строки. Однако в любом случае (будет ли lenS присвоен null или длина строки) переменная lenS будет иметь nullable-тип Int? .
Что если мы не хотим, чтобы lenS была nullable-типа, нам удобнее, чтобы она была обычного числового типа? В этом случае следует использовать так называемый элвис-оператор, обозначаемый вопросительным знаком, после которого идет двоеточие – ?: . Его работа чем-то схожа с if-else.
Если до элвиса получился null, вернется то, что после него, если нет – то, что до него.
В нашем примере в любом случае выражение s?.length ?: -1 дает целое число – либо длину строки, либо -1, а значит тип переменной lenS определяется как Int.
Еще одним способом избавления от null является оператор !! , который преобразует значение nullable-типа в аналогичное без поддержки null. Например:
Проблема этого оператора в том, что если nullable-переменная все же содержит null, произойдет выброс исключения уже на этапе исполнения программы:
Присвоить обычному типу значение null невозможно. Поэтому программа аварийно завершается с сообщением, что в потоке «main» произошло исключение типа NullPointerException. Однако если есть уверенность, что null быть не может, как например в случае функции readLine(), читающей строку с клавиатуры, то оператор !! может быть самым удобным способом преобразования:
В примере показано, что избавиться от nullable-типа можно на разных этапах. В первом случае мы сразу приводим строку к обычному типу, во втором – переменная s2 остается nullable, однако при вызове length создается копия значения, приводится к типу String и затем измеряется ее длина.
Напишите программу, которая запрашивает с ввода два числа, знак операции (+, -, * или /) и выполняет над числами указанную операцию. Для приведения строкового типа к числовому воспользуйтесь строковыми функциями toInt() или toDouble() и подобными.
Источник
Kotlin и работа с NULL
NullPointerException видимо настолько достал, что разработчики Kotlin решили раз и навсегда решить эту проблему наскоком. Теперь при инициализации переменной вы можете указать, что она в какой-то момент может быть null, и при ее использовании вы должны будете это учесть.
Давайте разберем вот этот довольно простой класс
У него есть два свойства — имя, которое не должно быть null, и никнейм, который может быть null (и даже больше — именно это значение указано при инициализации). Все довольно просто, а если вас вдруг смутил модификатор lateinit, то бояться его не стоит от слова совсем — просто предполагается, что перед использованием этого свойства оно будет проинициализировано (а иначе будет бум!)
Давайте сначала рассмотрим работу со свойством nick. Добавим в наш объект метод, определяющий, является ли никнейм одновременно адресом электронной почты
Компилятор сразу же указывает вам на ошибку, подчеркивая точку красным.
Что в переводе на язык начинающего kotlin разработчика означает, что можно использовать либо какой-то знак вопроса, либо двойное восклицание для данного объекта. Почему? А потому, что при описании свойства мы указали, что оно может быть null — об этом говорит знак вопроса после указания типа объекта.
Для того, чтобы компилятор пропустил наше выражение, необходимо его перестроить согласно подсказке. Использование знака вопроса предполагает безопасное использование, то есть если nick = null, то ничего выполняться не будет. В старой доброй Java без проверки на null мы бы получили NPE.
Получить желаемый результат для данного примера можно с использованием модификатора безопасного использования и оператора Элвиса. Смотрим
Читаем практически посимвольно:
Если свойство nick не null, то проверяем, есть ли в нем символ собаки. А если все-таки null, то это точно не email, так что в результате получим false (это то, что после непонятной комбинации “?:” )
Рассмотрим вторую подсказку, та, что с двойным восклицанием. А тут на самом деле все просто. Двойное восклицание в случае если nick = null генерирует старую добрую ошибку.
То есть считается, что это уже дается на ваш, как разработчика, откуп. Хотите — генерируйте NPE Exception. Зато вы точно будете знать где собака порылась. К слову, при конвертации проекта из Java в Kotlin двойное восклицание ставится в большинстве случаев, так что этот момент нужно обязательно учитывать.
К слову, хотя это не очень относится к нашей теме, но все же — если в задаче стоит, что если у вас nickname все таки email, то все что после собаки надо обрезать, то вам поможет функция let. Давайте сначала код, а потом уже разбор.
Значит так. Внутри функции let есть аргумент it, который является ни чем иным, как объектом, к которому применена сама функция. Особенностью этого метода является то, что то что будет записано в последней его строчке, будет являться результатом всей операции. То есть в нашем случае как раз таки — email это или нет. И в ней же мы модифицируем nick — режем все, что после собаки, включая ее саму.
Такая вот хитрость.
Идем дальше. Что там с необъявленным как возможный null свойством name?
Возможно, новая “ошибка на миллион” =)
При использовании не инициализированного свойства, объявленного как lateinit, приложение падает с ошибкой UninitializedPropertyAccessException.
Как решить? Вариантов масса и все они правильные. Если религия вам запрещает объявлять переменную как возможную null (чуть ниже расскажу почему такое может быть), то навскидку варианты следующие:
- Убрать модификатор lateinit и задать начальное значение переменной
Источник
Защита от null, ключевое слово let
В Java существует проблема с исключением NullPointerException и разработчику нужно постоянно устраивать проверки на null. В Kotlin существует защита от подобных ошибок. Нужно использовать вопросительный знак ? у типа.
Любой объект может быть null и мы можем явно указать это через символ вопроса ?. В этом случае Kotlin будет проверять на возможность ошибки и предупреждать на этапе разработки.
Любой тип, не поддерживающий null, является дочерним типом соответствующего типа, поддерживающего null, поэтому не путайте следующие выражения:
Умное приведение Smart cast позволяет превратить переменную из одного состояния в другое: cat? превратится в cat (см. пример выше) или переменная a типа Int? превратится в Int.
Если умное приведение вам не нужно, то используйте запись с безопасным оператором ?.
Функция будет вызвана только в том случае, если значение a отлично от null. Безопасные вызовы можно сцеплять.
Не путайте null с пустой строкой «». Выводим значение переменной типа String.
Любую функцию или параметр конструктора можно объявить с null-совместимым типом. Следующий код определяет функцию printInt(), которая получает параметр типа Int? (null-совместимый Int):
Функция может иметь null-совместимый возвращаемый тип.
Можно создать массив нулевых объектов. При этом массив может содержать и строки и null.
Элвис-оператор ?:
Другой «Элвис-оператор» ?: (напоминает причёску Элвиса Пресли, если повернуть голову, как на смайликах) позволяет назначить альтернативное значение, если объект равен null.
Справа от элвис-оператора можно использовать return и выбрасывать исключения.
Оператор !!
Если вы точно уверены, что ваша переменная не null, то можете использовать оператор !!. Kotlin будет полагаться на ваш профессионализм и не станет проверять ваше предположение. Если вы ошиблись в своём предположении, то ваше приложение может грохнуться.
Например, в Android часто объявляются компоненты, а инициализация происходит позже.
Подобный способ позволяет избежать null и аналогичен записи в Java.
При полной уверенности можете написать
Ключевое слово let
Ещё один способ избежать проблем с null — это использовать ключевое слово let вместе с оператором ?.:
Код будет выводить имя кота только в том случае, если объект не равен null.
Этот подход удобен, когда имеются смешанные варианты и мы хотим выполнения кода только для объектов, которые не имеют значения null:
В массиве три элемента, но на экран выводятся только два элемента.
Такой подход сокращает и упрощает код. Допустим, у нас есть функция getBestCat(), возвращающий тип Cat?.
Можем вызвать с проверкой на null.
А можно обойтись без создания новой переменной, а сразу использовать let:
Запись означает следующее: получить объект «Самый лучший кот», и если объект не null, то позволить ему есть (eat()).
Источник
Ключевые слова и операторы
Жесткие ключевые слова (Hard Keywords)
Следующие слова всегда интерпретируются как ключевые и не могут использоваться в качестве идентификаторов:
- as
- используется для приведения типов
- задает альтернативное имя для импорта
- as? используется для безопасного приведения типов
- break завершает выполнение цикла
- class объявляет класс
- continue переходит к следующему шагу ближайшего вложенного цикла
- do начинает цикл do/while loop (цикл с постусловием)
- else определяет ветвь выражения if, которое выполняется, когда условие ложно
- false указывает значение ‘false’ типа Boolean
- for начинает цикл for
- fun объявляет функцию
- if начинает условное выражение if
- in
- указывает объект, который перебирается в цикле for
- используется как инфиксный оператор для проверки того, что значение принадлежит диапазону, коллекции или другого объекта, который определяет метод ‘contains’
- используется в выражениях when с той же целью
- отмечает параметр типа как контравариантный
- !in
- используется в качестве оператора для проверки того, что значение не принадлежит диапазону, коллекции или другой объекта, который определяет метод ‘contains’
- используется в выражениях when с той же целью
- interface объявляет интерфейс
- is
- проверяет, что значение имеет определенный тип
- используется в выражениях when с той же целью
- !is
- проверяет, что значение не имеет определенный тип
- используется в выражениях when с той же целью
- null константа, представляющая ссылку на объект, которая не указывает ни на один объект
- object объявляет класс и его экземпляр одновременно
- package указывает пакет для текущего файла
- return по умолчанию производит возврат из ближайшей окружающей его функции или анонимной функции
- super
- ссылается на реализацию суперкласса метода или свойства
- вызывает конструктор суперкласса из вторичного конструктора
- this
- относится к текущему приемнику
- вызывает другой конструктор того же класса из вторичного конструктор
- throw вызывает исключение
- true задает значение ‘true’ типа Boolean
- try начинает блок обработки исключений
- typealias объявляет псевдоним типа
- val объявляет свойствотолько для чтения или локальную переменную
- var объявляет изменяемое свойство или локальную переменную
- when начинает выражение when (выполняет одну из заданных ветвей)
- while начинает цикл while (цикл с предусловием)
Мягкие Ключевые Слова (Soft Keywords)
Следующие слова действуют как ключевые в контексте, когда они применимы и могут использоваться как идентификаторы в других контекстах:
Модификаторы (Modifier Keywords)
Следующие слова действуют как ключевые в списках модификаторов объявлений и могут использоваться как идентификаторы в других контекстах:
- actual означает реализацию Платформы в мультиплатформенных проектах
- abstract обозначает класс или элемент как абстрактный
- annotation объявляет класс аннотации
- companion объявляет объект-компаньон
- const помечает свойство как константу времени компиляции
- crossinline запрещает нелокальные возвраты в лямбде, передаваемом встроенной функции
- data указывает компилятору генерировать канонические элементы для класса
- enum объявляет перечисление
- expect отмечает объявление как платформенное, ожидая реализации в модулях платформы.
- external отмечает объявление как реализованное не в Kotlin (доступное через JNI или JavaScript)
- final запрещает переопределение элемента
- infix позволяет вызывать функцию в инфиксной записи
- inline указывает компилятору встроить функцию и лямбда-выражение на стороне вызова
- inner позволяет ссылаться на экземпляр внешнего класса из вложенного класса
- internal помечает объявление как видимое в текущем модуле
- lateinit позволяет инициализацировать не-null свойство вне конструктора
- noinline отключает подстановку лямбды, переданной во встроенную функцию
- open позволяет создавать подклассы класса или переопределять элемент
- operator обозначает функцию как перегрузку оператора или реализацию соглашения
- out обозначает тип параметра как ковариантный
- override помечает элемент как переопределение элемента суперкласса
- private помечает объявление как видимое в текущем классе или файле
- protected помечает объявление как видимое в текущем классе и его подклассах
- public помечает декларацию как видимую в любом месте
- reified обозначает параметр типа встроенной функции, как доступная во время выполнения
- sealed объявляет изолированный класс (класс с ограниченным подклассом)
- suspend обозначает функцию или лямбда как приостанавливаемую (используется как сопрограмма)
- tailrec отмечает функцию как с хвостовой рекурсией (позволяя компилятору заменить рекурсию итерацией)
- vararg позволяет передавать нефиксированное число аргументов для параметра
Специальные идентификаторы (Special Identifiers)
Следующие идентификаторы определяются компилятором в определенных контекстах и могут использоваться как обычные идентификаторы в других контекстах:
- field используется внутри метода доступа свойства для ссылки на backing field свойства
- it используется внутри лямбды, чтобы косвенно ссылаться на ее параметр
Операторы и специальные символы (Operators and Special Symbols)
Котлин поддерживает следующие операторы и специальные символы:
Источник