--- title: Проекции типов description: Вариантность в месте использования, star-проекция и ограничения, которые она накладывает tags: - kotlin - generics --- [Вариантность](Вариантность.md) объявляется при определении класса. Но инвариантный класс изменить нельзя - ни свой `MutableList`, ни чужой из библиотеки. Для этого существуют проекции: вариантность в **месте использования**. ```kotlin fun copy(from: MutableList, to: MutableList) { for (item in from) to.add(item) } fun main() { val source = mutableListOf("a", "b") val target = mutableListOf(1) copy(source, target) println(target) // [1, a, b] } ``` Без `out` в сигнатуре `MutableList` не подошёл бы под `MutableList`. С `out` компилятор принимает любой `MutableList`, где `T` - подтип `Any`, но взамен запрещает использовать `from` на запись. ## out-проекция ```kotlin fun sumAll2(numbers: MutableList): Double { // numbers.add(1) // ошибка: add принимает Nothing return numbers.sumOf { it.toDouble() } } fun main() { println(sumAll2(mutableListOf(1, 2, 3))) // 6.0 println(sumAll2(mutableListOf(1.5, 2.5))) // 4.0 } ``` Внутри функции `MutableList` ведёт себя как `List`: читать можно, писать нельзя. Формально тип параметра `add` становится `Nothing` - значения такого типа не существует, поэтому вызвать его невозможно. ## in-проекция Зеркальный случай - нужно только писать: ```kotlin fun fillWithOnes(target: MutableList) { target.add(1) target.add(2) // val x: Int = target[0] // ошибка: читается как Any? val any: Any? = target[0] println("первый: $any") } fun main() { val numbers = mutableListOf(0.5) fillWithOnes(numbers) println(numbers) // [0.5, 1, 2] val anys = mutableListOf("x") fillWithOnes(anys) println(anys) // [x, 1, 2] } ``` `MutableList` принимает `MutableList`, `MutableList`, `MutableList` - любой список, в который `Int` поместится. Чтение даёт `Any?`, потому что компилятор не знает фактический тип элементов. | Проекция | Можно | Нельзя | Принимает | |---|---|---|---| | `MutableList` | читать как `T` | писать | `MutableList<подтип T>` | | `MutableList` | писать `T` | читать как `T` (только `Any?`) | `MutableList<супертип T>` | | `MutableList` | всё | - | только точно `MutableList` | Это ровно PECS из Java: **P**roducer - `out`, **C**onsumer - `in`. ## Копирование с обеими проекциями ```kotlin fun transfer(from: MutableList, to: MutableList) { for (item in from) to.add(item) } fun main() { val source = mutableListOf(1, 2, 3) val target = mutableListOf(0.0) transfer(source, target) println(target) // [0.0, 1, 2, 3] } ``` Здесь `from` - производитель, `to` - потребитель. Такая сигнатура максимально гибкая: подойдёт любая пара списков, где элементы источника помещаются в приёмник. ## Star-проекция Когда параметр типа не важен вообще, пишут `*`: ```kotlin fun describeSize(collection: Collection<*>): String = "элементов: ${collection.size}" fun main() { println(describeSize(listOf(1, 2))) // элементов: 2 println(describeSize(setOf("a"))) // элементов: 1 println(describeSize(mapOf("a" to 1).values)) // элементов: 1 } ``` `*` - это не «любой тип», а «неизвестный, но конкретный тип». Правила такие же, как у проекций, применённые сразу в обе стороны: ```kotlin fun main() { val list: MutableList<*> = mutableListOf(1, 2) println(list.size) // 2 — не зависит от T val item: Any? = list[0] // чтение даёт Any? println(item) // 1 // list.add(3) // ошибка: параметр имеет тип Nothing list.removeAt(0) // не требует T — разрешено println(list) // [2] } ``` | Объявление | `*` означает | |---|---| | `Foo` | чтение как `TUpper` | | `Foo` | запись как `Nothing` (невозможна) | | `Foo` | чтение `TUpper`, запись `Nothing` | ```kotlin fun main() { val map: Map<*, *> = mapOf("a" to 1) println(map.size) // 1 println(map.keys.first()) // a — тип Any? println(map.values.first()) // 1 — тип Any? val function: (Nothing) -> Any? = { "любая функция" } println(function::class.simpleName) // null — у лямбды нет имени класса } ``` ## Star-проекция и проверка типов Главное применение `*` - проверки `is`, где из-за стирания типов иначе никак: ```kotlin fun classify(value: Any): String = when (value) { is List<*> -> "список из ${value.size}" is Map<*, *> -> "карта из ${value.size}" is Set<*> -> "множество" is Array<*> -> "массив из ${value.size}" else -> "что-то ещё" } fun main() { println(classify(listOf(1, 2))) // список из 2 println(classify(mapOf("a" to 1))) // карта из 1 println(classify(arrayOf(1, 2, 3))) // массив из 3 println(classify(42)) // что-то ещё } ``` `is List` написать нельзя - компилятор откажется, потому что проверить это в рантайме невозможно. Подробнее о стирании: [Параметрический полиморфизм](Параметрический%20полиморфизм.md) Безопасная работа с содержимым после такой проверки: ```kotlin fun sumIfNumbers(value: Any): Double? { if (value !is List<*>) return null return value.filterIsInstance().sumOf { it.toDouble() } } fun main() { println(sumIfNumbers(listOf(1, 2.5))) // 3.5 println(sumIfNumbers(listOf(1, "не число"))) // 1.0 — лишнее отфильтровано println(sumIfNumbers("строка")) // null } ``` `filterIsInstance` проверяет каждый элемент по отдельности - это единственный честный способ сузить тип элементов после стирания. ## Когда проекция, когда параметр типа ```kotlin fun printFirst(items: List<*>) = println(items.firstOrNull()) fun firstOrNull3(items: List): T? = items.firstOrNull() fun main() { printFirst(listOf("a")) // a — но тип потерян val value: String? = firstOrNull3(listOf("a")) println(value?.length) // 1 — тип сохранён } ``` Правило: если тип нужен в возвращаемом значении - объявляйте ``. Проекция подходит, когда тип внутри функции действительно не используется. ```kotlin fun copyAll(from: List, to: MutableList) { to.addAll(from) } fun countAll(collections: List>): Int = collections.sumOf { it.size } fun main() { val target = mutableListOf() copyAll(listOf(1, 2), target) println(target) // [1, 2] println(countAll(listOf(listOf(1), setOf("a", "b")))) // 3 } ``` ## Проекции в своих классах ```kotlin class Container(private val items: MutableList) { fun add(item: T) { items.add(item) } fun get(index: Int): T = items[index] val size: Int get() = items.size fun addAllFrom(other: Container) { repeat(other.size) { items.add(other.get(it)) } } } fun main() { val numbers = Container(mutableListOf(0)) val ints = Container(mutableListOf(1, 2)) numbers.addAllFrom(ints) // Container подошёл под Container println(numbers.size) // 3 println(numbers.get(1)) // 1 } ``` Без `out` в `addAllFrom` пришлось бы либо делать класс ковариантным (невозможно - есть `add`), либо требовать точного совпадения типов. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `List` | `List` | > | `List` | `List` | > | `List` | `List<*>` | > | `Class` | `KClass<*>` | > | wildcard нужен почти всегда | проекция нужна редко | > | `? extends` на чтение, `? super` на запись | то же самое | > > **Это тот же механизм, что wildcard в Java.** `MutableList` и `List` означают одно и то же: неизвестный подтип, доступный только на чтение. Семантика, ограничения и правило PECS совпадают дословно. > > **Разница в том, как часто это нужно.** В Java `List` инвариантен, поэтому wildcard приходится писать почти в каждой сигнатуре, принимающей коллекцию: > ```java > double sum(Collection items) { ... } > void addAll(List dest) { ... } > ``` > В Kotlin `List` уже ковариантен, и проекция нужна только для изменяемых коллекций и собственных инвариантных классов: > ```kotlin > fun sum4(items: Collection) = items.sumOf { it.toDouble() } > > fun main() { println(sum4(listOf(1, 2))) } // 3.0 > ``` > Практическое следствие: `out`/`in` в сигнатурах Kotlin встречаются на порядок реже, чем wildcard в Java. Если вы пишете проекцию - стоит сначала проверить, не подойдёт ли read-only `List`. > > **Синтаксис короче и симметричен объявлению.** В Java `extends`/`super` в wildcard никак не связаны с объявлением класса. В Kotlin те же `out`/`in`, что и в declaration-site variance, что делает связь очевидной: проекция - это «применить вариантность здесь, раз её нет в классе». > > **`List<*>` и `List` ведут себя одинаково**, но `*` в Kotlin учитывает верхнюю границу параметра. Для `class Box` чтение из `Box<*>` даёт `Number`, а не `Object`: > ```kotlin > class NumberBox(val value: T) > > fun main() { > val box: NumberBox<*> = NumberBox(42) > val n: Number = box.value // Number, а не Any? > println(n.toDouble()) // 42.0 > } > ``` > Java в этой ситуации тоже выводит границу, но менее последовательно - через capture conversion, которая не всегда видна в сообщениях об ошибках. > > Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md) > [!warning] Подводные камни > **`out`-проекция запрещает запись полностью:** > ```kotlin > fun broken(items: MutableList) { > // items.add(1) // ошибка даже для Int > // items[0] = 1 // тоже ошибка > items.removeAt(0) // а это можно — не требует T > println(items.size) > } > > fun main() { broken(mutableListOf(1, 2)) } // 1 > ``` > Запрещены только методы, принимающие `T`. `clear`, `removeAt`, `size` работают - это иногда удивляет, потому что коллекция вроде бы «только для чтения». > > **`in`-проекция теряет тип при чтении:** > ```kotlin > fun readBack(items: MutableList) { > items.add("новый") > val value = items.last() // Any?, не String > println(value.toString().length) // приходится приводить > } > > fun main() { readBack(mutableListOf("a")) } // 6 > ``` > > **Проекция `*` не равна `Any?`:** > ```kotlin > fun main() { > val stars: MutableList<*> = mutableListOf(1, 2) > val anys: MutableList = mutableListOf(1, 2) > > // stars.add(3) // ошибка > anys.add(3) // работает > println("${stars.size} ${anys.size}") // 2 3 > } > ``` > `MutableList` - конкретный тип, в который можно класть что угодно. `MutableList<*>` - неизвестный тип, в который нельзя класть ничего. > > **`Array<*>` и `Array` тоже разные** - последний не примет `Array`, потому что массивы инвариантны. > > **После `is List<*>` элементы имеют тип `Any?`:** > ```kotlin > fun process4(value: Any) { > if (value is List<*>) { > // value.sumOf { it } // ошибка: it — Any? > println(value.filterIsInstance().sum()) > } > } > > fun main() { process4(listOf(1, 2, "x")) } // 3 > ``` > Умное приведение даёт только `List`. Содержимое нужно проверять поэлементно. > > **`is List` не компилируется, а `as List` компилируется с предупреждением** - и откладывает `ClassCastException` до момента использования: > ```kotlin > fun main() { > val any: Any = listOf("a", "b") > @Suppress("UNCHECKED_CAST") > val ints = any as List > println(ints.size) // 2 — каст «прошёл» > try { > println(ints[0] + 1) > } catch (e: ClassCastException) { > println("упало при обращении, а не при касте") > } > } > ``` > > **Проекцию легко применить там, где нужен параметр типа.** `fun first(items: List<*>): Any?` теряет тип результата; `fun first(items: List): T?` его сохраняет. Проекция - для случаев, когда тип внутри функции не нужен совсем. > > **Проекция `out` на уже ковариантном типе бессмысленна.** `List` компилируется, но ничего не добавляет к `List`, потому что `List` уже объявлен как `List`. Компилятор выдаёт предупреждение о лишней проекции. > > **Внутри класса `this` не проецируется.** Если метод принимает `Container`, то `other.add(x)` запрещён, но `this.add(x)` работает - проекция применена только к параметру. Далее: [Reified и inline](Reified%20и%20inline.md)