332 lines
16 KiB
Markdown
332 lines
16 KiB
Markdown
---
|
||
title: Проекции типов
|
||
description: Вариантность в месте использования, star-проекция и ограничения, которые она накладывает
|
||
tags:
|
||
- kotlin
|
||
- generics
|
||
---
|
||
|
||
[Вариантность](Вариантность.md) объявляется при определении класса. Но инвариантный класс изменить нельзя - ни свой `MutableList`, ни чужой из библиотеки. Для этого существуют проекции: вариантность в **месте использования**.
|
||
|
||
```kotlin
|
||
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-проекция
|
||
```kotlin
|
||
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-проекция
|
||
Зеркальный случай - нужно только писать:
|
||
```kotlin
|
||
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: **P**roducer - `out`, **C**onsumer - `in`.
|
||
|
||
## Копирование с обеими проекциями
|
||
```kotlin
|
||
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-проекция
|
||
Когда параметр типа не важен вообще, пишут `*`:
|
||
```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<out T : TUpper>` | чтение как `TUpper` |
|
||
| `Foo<in T>` | запись как `Nothing` (невозможна) |
|
||
| `Foo<T>` | чтение `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<Int>` написать нельзя - компилятор откажется, потому что проверить это в рантайме невозможно. Подробнее о стирании: [Параметрический полиморфизм](Параметрический%20полиморфизм.md)
|
||
|
||
Безопасная работа с содержимым после такой проверки:
|
||
```kotlin
|
||
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` проверяет каждый элемент по отдельности - это единственный честный способ сузить тип элементов после стирания.
|
||
|
||
## Когда проекция, когда параметр типа
|
||
```kotlin
|
||
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>`. Проекция подходит, когда тип внутри функции действительно не используется.
|
||
|
||
```kotlin
|
||
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
|
||
}
|
||
```
|
||
|
||
## Проекции в своих классах
|
||
```kotlin
|
||
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 приходится писать почти в каждой сигнатуре, принимающей коллекцию:
|
||
> ```java
|
||
> double sum(Collection<? extends Number> items) { ... }
|
||
> void addAll(List<? super String> dest) { ... }
|
||
> ```
|
||
> В Kotlin `List<out E>` уже ковариантен, и проекция нужна только для изменяемых коллекций и собственных инвариантных классов:
|
||
> ```kotlin
|
||
> fun sum4(items: Collection<Number>) = 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<T : Number>` чтение из `Box<*>` даёт `Number`, а не `Object`:
|
||
> ```kotlin
|
||
> 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](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md)
|
||
|
||
> [!warning] Подводные камни
|
||
> **`out`-проекция запрещает запись полностью:**
|
||
> ```kotlin
|
||
> 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`-проекция теряет тип при чтении:**
|
||
> ```kotlin
|
||
> 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?`:**
|
||
> ```kotlin
|
||
> 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?`:**
|
||
> ```kotlin
|
||
> 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` до момента использования:
|
||
> ```kotlin
|
||
> 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](Reified%20и%20inline.md)
|