18 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Вариантность | out и in в объявлении класса, ковариантность, контравариантность и правило PECS |
|
Вариантность отвечает на один вопрос: если String - подтип Any, то является ли Box<String> подтипом Box<Any>?
По умолчанию - нет. Дженерики инвариантны:
class Box6<T>(val value: T)
fun main() {
val str: Box6<String> = Box6("текст")
// val any: Box6<Any> = str // ошибка: несовместимые типы
println(str.value) // текст
}
Причина видна, если допустить обратное на изменяемой коллекции:
fun main() {
val strings = mutableListOf("a", "b")
// val anys: MutableList<Any> = strings // если бы компилировалось...
// anys.add(42) // ...сюда положили бы Int
// val s: String = strings[0] // ...и здесь получили бы ClassCastException
println(strings.size) // 2
}
Запрет инвариантности спасает от этой дыры. Но он же мешает там, где изменения нет.
out - ковариантность
out T означает, что T встречается только на выходе: в возвращаемых значениях, но не в параметрах.
class Producer<out T>(private val value: T) {
fun get(): T = value // T на выходе — разрешено
// fun set(v: T) { } // ошибка: T на входе запрещён
}
fun main() {
val strings: Producer<String> = Producer("текст")
val anys: Producer<Any> = strings // работает!
println(anys.get()) // текст
}
Это безопасно: из Producer<String> можно только читать, а всякая прочитанная String является Any.
Именно так объявлен List:
fun printSize(items: List<Any>) = items.size
fun main() {
println(printSize(listOf("a", "b"))) // 2
println(printSize(listOf(1, 2, 3))) // 3
val numbers: List<Number> = listOf(1, 2.0, 3L)
println(numbers.sumOf { it.toDouble() }) // 6.0
}
В stdlib: List<out E>, Set<out E>, Map<K, out V>, Iterable<out T>, Sequence<out T>, Pair<out A, out B>.
in - контравариантность
in T означает, что T встречается только на входе: в параметрах, но не в возвращаемых типах.
class Consumer<in T> {
fun consume(value: T) { println("принял $value") }
// fun produce(): T = TODO() // ошибка: T на выходе запрещён
}
fun main() {
val anyConsumer: Consumer<Any> = Consumer()
val stringConsumer: Consumer<String> = anyConsumer // работает!
stringConsumer.consume("текст") // принял текст
}
Направление перевёрнуто: Consumer<Any> является подтипом Consumer<String>. Логика простая - тот, кто умеет принимать любой Any, справится и со String.
Классический пример - Comparator:
fun main() {
val anyComparator = Comparator<Any> { a, b -> a.hashCode() - b.hashCode() }
val strings = mutableListOf("bb", "a")
strings.sortWith(anyComparator) // Comparator<Any> для List<String>
println(strings.size) // 2
val byLength: Comparator<CharSequence> = compareBy { it.length }
println(listOf("ccc", "a", "bb").sortedWith(byLength)) // [a, bb, ccc]
}
sortedWith принимает Comparator<in T>, поэтому компаратор по CharSequence годится для List<String>.
В stdlib: Comparator<in T>, Comparable<in T>, функциональные типы (in P) -> out R.
Сводка
| Объявление | Называется | T может быть |
Направление подтипов |
|---|---|---|---|
Box<T> |
инвариантность | везде | нет связи |
Box<out T> |
ковариантность | только возврат | Box<String> -> Box<Any> |
Box<in T> |
контравариантность | только параметр | Box<Any> -> Box<String> |
graph TD
subgraph co["out T: ковариантность"]
CS["Producer<String>"] --> CA["Producer<Any>"]
end
subgraph contra["in T: контравариантность"]
KA["Consumer<Any>"] --> KS["Consumer<String>"]
end
subgraph inv["T: инвариантность"]
IS["Box<String>"]
IA["Box<Any>"]
end
Функциональные типы
Вариантность функциональных типов - самый частый случай, с которым работают, не замечая этого:
fun main() {
val produceString: () -> String = { "текст" }
val produceAny: () -> Any = produceString // возврат ковариантен
println(produceAny()) // текст
val acceptAny: (Any) -> Unit = { println("принял $it") }
val acceptString: (String) -> Unit = acceptAny // параметр контравариантен
acceptString("строка") // принял строка
}
Тип (P) -> R объявлен как Function1<in P, out R>: функция, принимающая больше и возвращающая меньше, подходит везде, где ожидается более узкая.
Практическое следствие для API:
class EventBus {
private val handlers = mutableListOf<(Any) -> Unit>()
fun subscribe(handler: (Any) -> Unit) { handlers.add(handler) }
fun publish(event: Any) = handlers.forEach { it(event) }
}
fun main() {
val bus = EventBus()
bus.subscribe { println("получено: $it") }
bus.publish("событие") // получено: событие
bus.publish(42) // получено: 42
}
Когда помечать свой класс
Правило простое: если параметр типа используется только на выходе - ставьте out, только на входе - in. Компилятор проверит и подскажет.
interface Reader<out T> {
fun read(): T
fun readAll(count: Int): List<T> // List<T> — тоже выход, ок
}
interface Writer<in T> {
fun write(value: T)
fun writeAll(values: List<T>) // List<T> на входе, ок
}
interface Storage<T> : Reader<T>, Writer<T> // и то и другое — инвариантен
class MemoryStorage<T>(private var value: T) : Storage<T> {
override fun read(): T = value
override fun readAll(count: Int) = List(count) { value }
override fun write(value: T) { this.value = value }
override fun writeAll(values: List<T>) { values.lastOrNull()?.let { value = it } }
}
fun main() {
val storage = MemoryStorage("начальное")
val reader: Reader<Any> = storage // ковариантность работает
storage.write("новое")
println(reader.read()) // новое
println(reader.readAll(2)) // [новое, новое]
}
Разделение на Reader и Writer - типичный приём: инвариантный класс реализует оба интерфейса, а клиентам отдаётся вариантный.
Nothing и вариантность
Nothing - подтип всех типов, поэтому List<Nothing> подходит под любой List<T>:
fun main() {
val empty: List<Nothing> = emptyList()
val ints: List<Int> = empty // работает благодаря out
val strings: List<String> = empty
println("${ints.size} ${strings.size}") // 0 0
}
Именно поэтому emptyList() один на все типы - это синглтон EmptyList : List<Nothing>. Подробнее: Any, Unit, Nothing
Тот же приём в sealed-иерархиях:
sealed interface Outcome<out T> {
data class Ok<T>(val value: T) : Outcome<T>
data object Empty : Outcome<Nothing>
}
fun describe4(o: Outcome<Int>): String = when (o) {
is Outcome.Ok -> "значение ${o.value}"
Outcome.Empty -> "пусто"
}
fun main() {
println(describe4(Outcome.Ok(1))) // значение 1
println(describe4(Outcome.Empty)) // пусто
}
Empty может быть object, а не обобщённым классом, только потому что Outcome ковариантен. Подробнее: Sealed class и interface
Ограничение out: нельзя принимать T
Иногда ковариантный класс всё-таки должен принять T. Для этого параметр объявляют с более широкой границей:
class ImmutableBox<out T>(private val value: T) {
fun get(): T = value
fun <R> withDefault(default: R): R where R : Any = default
fun <U> plus(other: U): List<Any?> = listOf(value, other)
}
fun main() {
val box: ImmutableBox<String> = ImmutableBox("a")
val any: ImmutableBox<Any> = box
println(any.get()) // a
println(box.plus(1)) // [a, 1]
}
В stdlib этот приём виден в List.plus:
fun main() {
val strings: List<String> = listOf("a")
val mixed: List<Any> = strings + 1 // plus объявлен как <T> List<T>.plus(element: T)
println(mixed) // [a, 1]
}
Тип T здесь выводится как Any, а не как String, поэтому ковариантность не нарушается.
[!info] Отличия от Java
Java Kotlin List<? extends T>в каждой сигнатуреList<out E>один раз в объявленииList<? super T>MutableList<in E>илиinв объявленииCollection<?>Collection<*>нет declaration-site variance out/inпри объявлении классаPECS - правило для программиста компилятор проверяет сам Главное отличие - где объявляется вариантность. Java знает только use-site variance: интерфейс
List<E>инвариантен, и каждый метод, принимающий «список чего-то производного», обязан писать wildcard:void printAll(Collection<? extends Object> items) { ... } double sum(List<? extends Number> numbers) { ... } void addAll(List<? super Integer> dest, List<? extends Integer> src) { ... }В Kotlin
List<out E>объявлен один раз в stdlib, и дальше всё работает само:fun sum3(numbers: List<Number>) = numbers.sumOf { it.toDouble() } fun main() { println(sum3(listOf(1, 2, 3))) // 6.0 — List<Int> подошёл }Это убирает wildcard из подавляющего большинства сигнатур. Use-site variance в Kotlin тоже есть - это проекции типов, но нужны они редко.
PECS из правила превратился в проверку компилятора. В Java «Producer Extends, Consumer Super» - мнемоника, которую программист применяет вручную; забыл - и метод неудобен в использовании, но компилируется. В Kotlin вы помечаете класс
out, и компилятор откажется принятьTв параметр: ошибка возникает при объявлении, а не у пользователя API.
Comparatorв Kotlin контравариантен автоматически. В JavaComparator<T>инвариантен, и методы вродеCollections.sortобъявляютComparator<? super T>. В Kotlin stdlibComparatorужеin T, поэтомуsortedWith(comparator)принимает компаратор по любому супертипу без дополнительных обозначений.Разделение
List/MutableListделаетoutвозможным. В JavaListсодержитadd, поэтому ковариантным он быть не может в принципе - только через wildcard в месте использования. Kotlin вынес мутаторы вMutableList<E>(инвариантный), иList<out E>стал безопасным по построению.Сравнить: Параметрический полиморфизм в Java, Полиморфизм
[!warning] Подводные камни
outзапрещаетTв параметрах - включая неочевидные места:class Holder<out T>(private val value: T) { fun get(): T = value // fun contains(other: T): Boolean = value == other // ошибка fun contains(other: Any?): Boolean = value == other // так можно } fun main() { println(Holder("a").contains("a")) // true }Расширение границы до
Any?- стандартный обход, но оно теряет проверку типов:contains(42)тоже скомпилируется.
privateфункции могут нарушать вариантность:class Mutable<out T>(private var value: T) { fun get(): T = value private fun set(v: T) { value = v } // private — проверка не применяется } fun main() { println(Mutable("a").get()) } // aКомпилятор разрешает
Tна входе вprivateчленах, потому что снаружи они недоступны. Ноprivate var value: T- всё ещё изменяемое состояние; если его изменить изнутри класса, ковариантный «снимок» окажется нестабильным.
MutableListинвариантен, и это не ошибка:fun main() { val strings = mutableListOf("a") // val anys: MutableList<Any> = strings // не скомпилируется — и правильно val readOnly: List<Any> = strings // а так можно println(readOnly.size) // 1 }Если нужно передать изменяемую коллекцию «как список чего угодно» - передавайте
List, а неMutableList.Массивы инвариантны в Kotlin и ковариантны в Java:
fun main() { val strings = arrayOf("a", "b") // val anys: Array<Any> = strings // ошибка компиляции println(strings.size) // 2 }В Java
String[]являетсяObject[], поэтомуObject[] o = new String[1]; o[0] = 1;компилируется и бросаетArrayStoreExceptionв рантайме. Kotlin закрыл эту дыру ценой того, чтоArray<Any>не принимаетArray<String>. Для чтения используйтеArray<out T>.Вариантность не распространяется на вложенные параметры автоматически:
fun main() { val nested: List<List<String>> = listOf(listOf("a")) val widened: List<List<Any>> = nested // работает: оба List ковариантны println(widened.size) // 1 val maps: List<MutableList<String>> = listOf(mutableListOf("a")) // val wide: List<MutableList<Any>> = maps // не работает: MutableList инвариантен println(maps.size) // 1 }
inиoutлегко перепутать направление. Мнемоника:out- значение выходит из объекта (producer, можно только читать),in- значение входит в объект (consumer, можно только писать). Если нужны обе операции, вариантность невозможна.
Map<K, out V>ковариантен только по значению:fun main() { val m: Map<String, Int> = mapOf("a" to 1) val widened: Map<String, Any> = m // ок — V ковариантен // val keys: Map<Any, Int> = m // ошибка — K инвариантен println(widened.size) // 1 }Ключ инвариантен, потому что он используется на входе в
get(key).Ковариантный класс с
var-полем типаTобъявить нельзя - сеттер принимаетT. Если нужно изменяемое состояние, либо инвариантность, либо приватный сеттер с осознанием последствий.
Далее: Проекции типов