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

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

А также проводим аналогии с Java
5 минут1920

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

Unit

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

fun knockKnock(){
 println("Who’s there?")
}

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

fun knockKnock(): Unit = println("Who’s there?")

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

public object Unit {
   override fun toString() = "kotlin.Unit"
}

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

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

Nothing

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

public class Nothing private constructor()

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

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

public inline fun TODO(): Nothing = throw NotImplementedError()

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

override fun getData(word: String): List<Data> {
   TODO("not implemented")
}

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

fun doSomething(): Something = TODO()

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

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

static Void todo(){
 throw new RuntimeException("Not Implemented");
}
 
String myMethod(){  
   return todo(); 
}

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

fun getData(): Data? = ...

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

fun getData(onError: (SomeError) -> Nothing): Data = ...

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

val data = getData() { err ->
       when (err) {
           is InvalidStatement -> throw Exception(err.parseError)
           is NoSuchData -> ...
       }
   }
   return Data() //успешный сценарий
}

Закрепим:

//Скомпилируются нормально
fun funOne(): Unit { while (true) {} }
fun funTwo(): Nothing { while (true) {} }
 
 
//Ок
fun funThree(): Unit { println("hi") }
//Не ок
fun funFour(): Nothing { println("hi") } 

Any

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

public open class Any {
   public open operator fun equals(other: Any?): Boolean
   public open fun hashCode(): Int
   public open fun toString(): String
}

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

Null

В Kotlin null может формировать nullable-типы (произносится как «ну́ллабл»). Они обозначаются добавлением знака ? в конце типа. Например, String? — это тип String + null. То есть значение может быть строковым, а может быть null. В Java такие дополнения не нужны — там любой объект может быть null, и это приводит нас к одному из преимуществ языка Kotlin перед Java — null safety.

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

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

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

var notNullable: String = ""
notNullable = null // Ошибка компиляции

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

var nullable: String? = " "

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

nontNullable = nullable // Ошибка компиляции

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

val length = nullable.length // Ошибка компиляции

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

if (nullable != null) {
    length = nullable.length // OK
    nonNullable = nullable   // OK
}

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

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

var name: String = "Иван"
var fullName: String? = ""
fun checkStirng(s: String): String? {
    ...    
}
 
checkString(fullName)  // Ошибка компиляции
name = checkString(name) // Ошибка компиляции

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

class Person {
   Preson(String firstName, String secondName, int age) {...}
}
 
Person person = new Person(null, null, 29);
person.getFirstName().length()

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

class Person(firstName : String, secondName : String, age : Int)
val person = Person(null, null, 29)  // ошибка компиляции

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

class Person(firstName : String?, secondName : String?, age : Int?)
val person = Person(null, null, null)  // Всë ок!

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

Каждый раз писать проверку на null, “if (s != null)...” не выглядит привлекательным. И Kotlin предоставляет более удобные инструменты для работы с nullable-типами. Поговорим о них.

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

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

val name: String? = "John"
val nameLength: Int? = name?.length

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

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

Ещё один удобный оператор для работы с null safety. «Элвис» записывают как ?: (знак вопроса с последующим двоеточием). Если задействовать воображение и посмотреть на запись под углом, можно найти сходство с образом Пресли:

val nameLength: Int = name?.length ?: 1

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

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

программированиеandroid
Нашли ошибку в тексте? Напишите нам.
Спасибо,
что читаете наш блог!
Posts popup