Files
obsidian/Kotlin/Синтаксис и типы/Базовые типы ООП/Any, Unit, Nothing.md
T

11 KiB
Raw Blame History

title, description, tags
title description tags
Any, Unit, Nothing Корень иерархии Any, тип-единица Unit и нижний тип Nothing
kotlin
types

Три особых типа образуют каркас системы типов Kotlin.

Тип Сколько значений Роль
Any все объекты супертип всего
Unit одно (Unit) «результата нет»
Nothing ни одного «возврата не будет»
graph TD
    A["Any?"] --> B["Any"]
    A --> N1["Nothing?"]
    B --> C["String"]
    B --> D["Int"]
    B --> E["Unit"]
    C --> N["Nothing"]
    D --> N
    E --> N
    N1 --> N

Any сверху, Nothing снизу: он подтип любого типа.

Any

Супертип всех non-null типов. Объявляет три метода:

fun main() {
    val x: Any = 42
    println(x.toString())      // 42
    println(x.hashCode())      // 42
    println(x == 42)           // true — вызов equals
}

Больше у Any ничего нет: ни wait, ни notify, ни getClass.

fun main() {
    val items: List<Any> = listOf(1, "два", 3.0, true)
    println(items.joinToString { it::class.simpleName ?: "?" })
    // Int, String, Double, Boolean
}

Any не принимает null - для этого есть Any?, самый широкий тип в языке:

fun main() {
    // val a: Any = null     // ошибка компиляции
    val b: Any? = null       // ok
    println(b)               // null
}

Расширения Any? можно звать на null:

fun main() {
    val x: String? = null
    println(x.toString())      // "null" — расширение Any?.toString()
    println(x.hashCode())      // 0
}

Это не методы Any, а функции-расширения stdlib, объявленные для Any?.

Unit

Тип функций, которые ничего не возвращают. Это настоящий объект, а не отсутствие значения:

fun log(msg: String): Unit = println(msg)

fun main() {
    val result = log("привет")
    println(result)              // kotlin.Unit
    println(result == Unit)      // true
}

Unit - object, у него ровно одно значение. Тип результата Unit можно опускать, и обычно так и делают:

fun log(msg: String) = println(msg)        // тип выведен: Unit
fun log2(msg: String) { println(msg) }     // то же самое

Поскольку Unit - полноценный тип, он подставляется в дженерики, где void в Java не работает:

fun main() {
    val actions: List<() -> Unit> = listOf({ print("a") }, { print("b") })
    actions.forEach { it() }      // ab
    println()

    val map = mutableMapOf<String, Unit>()
    map["ключ"] = Unit            // Unit как значение — законно
    println(map)                  // {ключ=kotlin.Unit}
}

В лямбде с типом результата Unit последнее выражение игнорируется - возвращать ничего не нужно:

fun run2(block: () -> Unit) = block()

fun main() {
    run2 { 42 }        // 42 отбрасывается, лямбда подходит под () -> Unit
    println("ok")
}

Nothing

Тип без значений. Функция с результатом Nothing не может завершиться нормально - только исключением или бесконечным циклом:

fun fail(message: String): Nothing = throw IllegalStateException(message)

fun main() {
    val x: String = if (true) "ok" else fail("нет значения")
    println(x)      // ok
}

Зачем это нужно: Nothing - подтип любого типа, поэтому выражение с типом Nothing подходит в любую позицию. Компилятор знает, что дальше кода не будет.

fun fail(msg: String): Nothing = throw IllegalArgumentException(msg)

fun parse(s: String): Int = s.toIntOrNull() ?: fail("не число: $s")

fun main() {
    println(parse("42"))      // 42
    // parse("abc")           // IllegalArgumentException: не число: abc
}

Если бы fail возвращала Unit, этот код не скомпилировался бы: Int и Unit несовместимы.

throw сам по себе - выражение типа Nothing:

fun main() {
    val x: Int = try {
        "abc".toInt()
    } catch (e: NumberFormatException) {
        throw IllegalStateException("плохие данные", e)   // Nothing подходит
    }
    println(x)
}

Анализ недостижимого кода

fun fail(): Nothing = throw RuntimeException()

fun main() {
    val x = 1
    if (x > 0) {
        println("да")
    } else {
        fail()
        // println("недостижимо")   // предупреждение компилятора
    }
}

Компилятор использует Nothing, чтобы доказать: ветка не продолжается. Поэтому TODO() работает где угодно:

fun calculate(x: Int): String = TODO("ещё не реализовано")

fun main() {
    // calculate(1)    // NotImplementedError
    println("компилируется")
}

TODO() объявлена как fun TODO(): Nothing, поэтому подходит под любой тип результата.

Nothing? и null

fun main() {
    val x = null
    println(x)                      // null
    // тип x — Nothing?, единственное значение которого null
    val list = listOf(null, null)   // List<Nothing?>
    println(list.size)              // 2
}

Пустые коллекции тоже используют Nothing:

fun main() {
    val empty: List<Nothing> = emptyList()
    val strings: List<String> = empty     // подходит — List ковариантен
    println(strings.size)                  // 0
}

Один объект EmptyList подходит под List<T> для любого T именно потому, что List<Nothing> - подтип всех List<T>.

[!info] Отличия от Java

Java Kotlin
Object Any (но без wait/notify/getClass)
void Unit
Void Unit (или Nothing?)
нет аналога Nothing
Object o = null; Any? = null, Any не принимает null

Any беднее Object. Методы wait(), notify(), getClass() в Any не объявлены: первые два относятся к устаревшей модели синхронизации, третий заменён на ::class. На JVM они доступны после приведения к Object, но из Kotlin их намеренно не видно.

Unit против void - не косметика. void не тип, поэтому в Java нельзя написать List<void> или Function<String, void>; приходится использовать Void (у которого единственное значение - null) или Consumer. В Kotlin () -> Unit - обычный тип.

Nothing в Java нет вообще. Из-за этого Java-компилятор не знает, что метод никогда не вернёт управление:

int parse(String s) {
    if (s == null) fail("null");   // fail объявлен void
    return Integer.parseInt(s);     // компилятор требует этот return
}

В Kotlin fail(): Nothing сообщает компилятору, что ветка обрывается, и анализ потока это учитывает.

Сравнить: Класс Object в Java

[!warning] Подводные камни Any - не то же самое, что Any?. Функция, принимающая Any, не примет null:

fun show(x: Any) = x.toString()

fun main() {
    val n: String? = null
    // println(show(n))      // ошибка компиляции
    println(show(n ?: "пусто"))   // пусто
}

Лямбда с результатом Unit молча глотает значение:

fun main() {
    val items = mutableListOf<Int>()
    listOf(1, 2).forEach { items.add(it) }   // add возвращает Boolean,
    println(items)                            // но тип () -> Unit его отбрасывает
}

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

return с типом Unit писать не нужно, а return Unit - признак непонимания:

fun f(): Unit {
    return Unit        // компилируется, но так не пишут
}

Функция с типом Nothing не может иметь тело-блок с return:

fun fail(): Nothing {
    // return                 // ошибка: ничего нельзя вернуть
    throw RuntimeException()  // только выход через исключение
}

val x = null имеет тип Nothing?, а не Any?. Присвоить такой переменной что-либо другое нельзя:

fun main() {
    var x = null
    // x = "строка"      // ошибка: ожидался Nothing?
    val y: String? = null  // так
    println("$x $y")
}

Nothing в дженерике означает «значений не будет»:

fun main() {
    val producer: () -> Nothing = { throw RuntimeException("всегда падает") }
    // producer()    — вызов гарантированно бросит исключение
    println("тип ${producer::class.simpleName} объявлен, но вызов невозможен")
}

Если такой тип получился случайно (например, emptyList() без указания параметра), в коллекцию ничего нельзя добавить.

Дополнительно: equals, hashCode и toString Далее: Data class