16 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Проекции типов | Вариантность в месте использования, star-проекция и ограничения, которые она накладывает |
|
Вариантность объявляется при определении класса. Но инвариантный класс изменить нельзя - ни свой MutableList, ни чужой из библиотеки. Для этого существуют проекции: вариантность в месте использования.
fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
for (item in from) to.add(item)
}
fun main() {
val source = mutableListOf("a", "b")
val target = mutableListOf<Any>(1)
copy(source, target)
println(target) // [1, a, b]
}
Без out в сигнатуре MutableList<String> не подошёл бы под MutableList<Any>. С out компилятор принимает любой MutableList<T>, где T - подтип Any, но взамен запрещает использовать from на запись.
out-проекция
fun sumAll2(numbers: MutableList<out Number>): 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<out Number> ведёт себя как List<Number>: читать можно, писать нельзя. Формально тип параметра add становится Nothing - значения такого типа не существует, поэтому вызвать его невозможно.
in-проекция
Зеркальный случай - нужно только писать:
fun fillWithOnes(target: MutableList<in Int>) {
target.add(1)
target.add(2)
// val x: Int = target[0] // ошибка: читается как Any?
val any: Any? = target[0]
println("первый: $any")
}
fun main() {
val numbers = mutableListOf<Number>(0.5)
fillWithOnes(numbers)
println(numbers) // [0.5, 1, 2]
val anys = mutableListOf<Any>("x")
fillWithOnes(anys)
println(anys) // [x, 1, 2]
}
MutableList<in Int> принимает MutableList<Int>, MutableList<Number>, MutableList<Any> - любой список, в который Int поместится. Чтение даёт Any?, потому что компилятор не знает фактический тип элементов.
| Проекция | Можно | Нельзя | Принимает |
|---|---|---|---|
MutableList<out T> |
читать как T |
писать | MutableList<подтип T> |
MutableList<in T> |
писать T |
читать как T (только Any?) |
MutableList<супертип T> |
MutableList<T> |
всё | - | только точно MutableList<T> |
Это ровно PECS из Java: Producer - out, Consumer - in.
Копирование с обеими проекциями
fun <T> transfer(from: MutableList<out T>, to: MutableList<in T>) {
for (item in from) to.add(item)
}
fun main() {
val source = mutableListOf(1, 2, 3)
val target = mutableListOf<Number>(0.0)
transfer(source, target)
println(target) // [0.0, 1, 2, 3]
}
Здесь from - производитель, to - потребитель. Такая сигнатура максимально гибкая: подойдёт любая пара списков, где элементы источника помещаются в приёмник.
Star-проекция
Когда параметр типа не важен вообще, пишут *:
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
}
* - это не «любой тип», а «неизвестный, но конкретный тип». Правила такие же, как у проекций, применённые сразу в обе стороны:
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<out T : TUpper> |
чтение как TUpper |
Foo<in T> |
запись как Nothing (невозможна) |
Foo<T> |
чтение TUpper, запись Nothing |
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, где из-за стирания типов иначе никак:
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<Int> написать нельзя - компилятор откажется, потому что проверить это в рантайме невозможно. Подробнее о стирании: Параметрический полиморфизм
Безопасная работа с содержимым после такой проверки:
fun sumIfNumbers(value: Any): Double? {
if (value !is List<*>) return null
return value.filterIsInstance<Number>().sumOf { it.toDouble() }
}
fun main() {
println(sumIfNumbers(listOf(1, 2.5))) // 3.5
println(sumIfNumbers(listOf(1, "не число"))) // 1.0 — лишнее отфильтровано
println(sumIfNumbers("строка")) // null
}
filterIsInstance проверяет каждый элемент по отдельности - это единственный честный способ сузить тип элементов после стирания.
Когда проекция, когда параметр типа
fun printFirst(items: List<*>) = println(items.firstOrNull())
fun <T> firstOrNull3(items: List<T>): T? = items.firstOrNull()
fun main() {
printFirst(listOf("a")) // a — но тип потерян
val value: String? = firstOrNull3(listOf("a"))
println(value?.length) // 1 — тип сохранён
}
Правило: если тип нужен в возвращаемом значении - объявляйте <T>. Проекция подходит, когда тип внутри функции действительно не используется.
fun <T> copyAll(from: List<T>, to: MutableList<in T>) {
to.addAll(from)
}
fun countAll(collections: List<Collection<*>>): Int = collections.sumOf { it.size }
fun main() {
val target = mutableListOf<Any>()
copyAll(listOf(1, 2), target)
println(target) // [1, 2]
println(countAll(listOf(listOf(1), setOf("a", "b")))) // 3
}
Проекции в своих классах
class Container<T>(private val items: MutableList<T>) {
fun add(item: T) { items.add(item) }
fun get(index: Int): T = items[index]
val size: Int get() = items.size
fun addAllFrom(other: Container<out T>) {
repeat(other.size) { items.add(other.get(it)) }
}
}
fun main() {
val numbers = Container<Number>(mutableListOf(0))
val ints = Container(mutableListOf(1, 2))
numbers.addAllFrom(ints) // Container<Int> подошёл под Container<out Number>
println(numbers.size) // 3
println(numbers.get(1)) // 1
}
Без out в addAllFrom пришлось бы либо делать класс ковариантным (невозможно - есть add), либо требовать точного совпадения типов.
[!info] Отличия от Java
Java Kotlin List<? extends T>List<out T>List<? super T>List<in T>List<?>List<*>Class<?>KClass<*>wildcard нужен почти всегда проекция нужна редко ? extendsна чтение,? superна записьто же самое Это тот же механизм, что wildcard в Java.
MutableList<out Number>иList<? extends Number>означают одно и то же: неизвестный подтип, доступный только на чтение. Семантика, ограничения и правило PECS совпадают дословно.Разница в том, как часто это нужно. В Java
Listинвариантен, поэтому wildcard приходится писать почти в каждой сигнатуре, принимающей коллекцию:double sum(Collection<? extends Number> items) { ... } void addAll(List<? super String> dest) { ... }В Kotlin
List<out E>уже ковариантен, и проекция нужна только для изменяемых коллекций и собственных инвариантных классов:fun sum4(items: Collection<Number>) = items.sumOf { it.toDouble() } fun main() { println(sum4(listOf(1, 2))) } // 3.0Практическое следствие:
out/inв сигнатурах Kotlin встречаются на порядок реже, чем wildcard в Java. Если вы пишете проекцию - стоит сначала проверить, не подойдёт ли read-onlyList.Синтаксис короче и симметричен объявлению. В Java
extends/superв wildcard никак не связаны с объявлением класса. В Kotlin те жеout/in, что и в declaration-site variance, что делает связь очевидной: проекция - это «применить вариантность здесь, раз её нет в классе».
List<*>иList<?>ведут себя одинаково, но*в Kotlin учитывает верхнюю границу параметра. Дляclass Box<T : Number>чтение изBox<*>даётNumber, а неObject:class NumberBox<T : Number>(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
[!warning] Подводные камни
out-проекция запрещает запись полностью:fun broken(items: MutableList<out Number>) { // 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-проекция теряет тип при чтении:fun readBack(items: MutableList<in String>) { items.add("новый") val value = items.last() // Any?, не String println(value.toString().length) // приходится приводить } fun main() { readBack(mutableListOf("a")) } // 6Проекция
*не равнаAny?:fun main() { val stars: MutableList<*> = mutableListOf(1, 2) val anys: MutableList<Any?> = mutableListOf(1, 2) // stars.add(3) // ошибка anys.add(3) // работает println("${stars.size} ${anys.size}") // 2 3 }
MutableList<Any?>- конкретный тип, в который можно класть что угодно.MutableList<*>- неизвестный тип, в который нельзя класть ничего.
Array<*>иArray<Any?>тоже разные - последний не приметArray<String>, потому что массивы инвариантны.После
is List<*>элементы имеют типAny?:fun process4(value: Any) { if (value is List<*>) { // value.sumOf { it } // ошибка: it — Any? println(value.filterIsInstance<Int>().sum()) } } fun main() { process4(listOf(1, 2, "x")) } // 3Умное приведение даёт только
List<Any?>. Содержимое нужно проверять поэлементно.
is List<Int>не компилируется, аas List<Int>компилируется с предупреждением - и откладываетClassCastExceptionдо момента использования:fun main() { val any: Any = listOf("a", "b") @Suppress("UNCHECKED_CAST") val ints = any as List<Int> println(ints.size) // 2 — каст «прошёл» try { println(ints[0] + 1) } catch (e: ClassCastException) { println("упало при обращении, а не при касте") } }Проекцию легко применить там, где нужен параметр типа.
fun first(items: List<*>): Any?теряет тип результата;fun <T> first(items: List<T>): T?его сохраняет. Проекция - для случаев, когда тип внутри функции не нужен совсем.Проекция
outна уже ковариантном типе бессмысленна.List<out String>компилируется, но ничего не добавляет кList<String>, потому чтоListуже объявлен какList<out E>. Компилятор выдаёт предупреждение о лишней проекции.Внутри класса
thisне проецируется. Если метод принимаетContainer<out T>, тоother.add(x)запрещён, ноthis.add(x)работает - проекция применена только к параметру.
Далее: Reified и inline