Представлено в Еженедельном выпуске Kotlin № 200

Обнуляемость Kotlin: четыре простых элемента

Одна из самых полезных функций Kotlin - это обнуление. В основном система типов Kotlin позволяет вам определять тип как допускающий или не допускающий значение NULL. Когда вы это сделаете, компилятор будет следить за тем, чтобы все проверки выполнялись перед запуском кода, чтобы вы могли избежать печально известного NullPointerException.

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

1. Вызов функции для нулевого объекта

Рассмотрим этот простой фрагмент:

var myString: String? = null
val numberOfWhiteSpaces: Int = myString.countWhiteSpaces()

Теперь вы можете подумать, что приведенный выше код не будет компилироваться, потому что мы не выполняем никаких проверок объекта, допускающего значение NULL, например, с помощью оператора безопасного вызова (?.) или утверждения ненулевого (!!), но на самом деле это может быть не всегда так. Посмотрим почему!

Как некоторые из вас могли заметить, countWhiteSpaces() не является стандартным методом String. Тогда это должна быть функция расширения, о которой я не упомянул специально, чтобы вас обмануть. Правильно, определение функции расширения для типа, допускающего значение NULL, совершенно законно. Вот код:

fun String?.countWhiteSpaces(): Int {
    return this?.toList()?.count { it.isWhitespace() } ?: 0
}

Как видите, получатель String? допускает значение NULL. И теперь проблема допустимости значений NULL делегирована функции расширения.

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

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

2. Типы платформ

Рассмотрим этот фрагмент:

data class Developer(
        val name: String, 
        val age: Int,
        val mainLanguage: String
)
service.getCurrentDeveloper().name

Предположим, что служба возвращает объект Developer. Теперь, если этот код компилируется, разумно думать, что service.getCurrentDeveloper() возвращает объект ненулевой, что также означает, что во время выполнения нас не будет ничего удивительного.
На самом деле это не так. . Действительно, во время выполнения может произойти сбой из-за допустимости значений NULL.
Давайте посмотрим, почему!

К настоящему времени вы должны меня знать: я снова опустил деталь, чтобы обмануть вас: служба написана на Java, поэтому в компиляторе Kotlin отсутствует информация о допустимости значений NULL. Это Java-код моей службы:

public class Service {
    public Developer getCurrentDeveloper() {
        return dbDeveloper.fetchCurrent()
    }
}

Kotlin не делает вывод, что переменная Developer может иметь значение NULL или ненулевое значение. Вместо этого переменная Developer распознается как тип платформы.

Тип платформы - это, по сути, тип, с которым можно работать как с типом, допускающим значение NULL, так и с типом ненулевым. Это дает разработчику ответственность за правильные действия, как в Java. IDE указывает тип платформы восклицательным знаком в конце (Developer!)

Однако есть способ заставить компилятор Kotlin понять, как мы хотим обрабатывать эту переменную. Поскольку мы знаем, что наш метод getCurrentDeveloper может возвращать объект null, мы можем сообщить об этом компилятору Kotlin, добавив аннотацию @Nullable:

public class Service {
    @Nullable
    public Developer getCurrentDeveloper() {
        return dbDeveloper.fetchCurrent()
    }
}

Теперь следующая строка больше не будет компилироваться:

service.getCurrentDeveloper().name

Нам нужно использовать оператор безопасного вызова, или мы должны попытаться преобразовать его в ненулевое значение!!):

service.getCurrentDeveloper()?.name

Мы также можем использовать аннотацию @NonNull, чтобы Kotlin ожидал тип, не допускающий значения NULL.

3. Обнуляемые функции

Функции Kotlin - первоклассный гражданин, поэтому их можно хранить в переменных. Например:

var myFun: (String) -> Int = {
     ...
}

это означает, что мы можем выполнить функцию таким образом:

myFun("my string")

Однако также можно определить функцию, чтобы ее тип мог быть null:

var myFun: ((String) -> Int)? = null

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

if(myFun != null) {
    myFun("my string")
}

Или, более идиоматично и кратко:

myFun?.invoke("my string")

4. Возможность обнуления параметра универсального типа

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

fun <T> getClassLength(t: T): String {
    return t.javaClass.getName()
}

Приведенный выше код не компилируется. Причина в том, чтоT включает все возможные типы, включая типы null. Таким образом, вы можете передать Int, но также и Int?.
Конечно, вы можете проверить, не имеет ли объект t значение null, а затем выполнить свое действие. Но что, если мы не хотим разрешать типы, допускающие значение NULL? Выход есть:

fun <T: Any> getClassLength(t: T) : String {
    return t.javaClass.getName()
}

Мы можем ограничить разрешенный тип до Any, что исключает типы, допускающие значение NULL. Итак, нужно помнить, что <T> эквивалентно веселью <T: Any?>, поэтому, если вы не хотите разрешать типы, допускающие значение NULL, вы должны быть явными, используя <T: Any>

Мы видели, что даже если Kotlin очень помогает в решении проблемы допустимости значений NULL, важно помнить, что ответственность за допустимость значений NULL по-прежнему лежит на разработчике.

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