Обобщения: in, out, where
Классы в Kotlin могут иметь параметры типа, как и в Java:
class Box<T>(t: T) {
var value = t
}
Для создания экземпляра такого класса просто укажите аргументы типа:
val box: Box<Int> = Box<Int>(1)
Но если параметры можно определить, например, из аргументов конструктора, вы можете опустить аргументы типа:
val box = Box(1) // 1 has type Int, so the compiler figures out that it is Box<Int>
Изменчивость
Один из самых сложных аспектов системы типов Java — это типы с подстановкой (см. Java Generics FAQ). В Kotlin таких нет. Вместо этого Kotlin использует изменчивость на уровне объявления и проекции типов.
Давайте подумаем, почему Java нуждается в этих загадочных подстановках. Проблема хорошо объясняется в Effective Java, 3-е издание, пункт 31: Используйте ограниченные подстановки для повышения гибкости API. Во-первых, обобщённые типы в Java являются инвариантными, что означает, что List<String> не является подтипом List<Object>. Если List не был инвариантным, это было бы не лучше, чем массивы в Java, так как следующий код был бы компилируемым, но вызвал бы исключение во время выполнения:
// Java List<String> strs = new ArrayList<String>(); List<Object> objs = strs; // !!! A compile-time error here saves us from a runtime exception later. objs.add(1); // Put an Integer into a list of Strings String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
Java запрещает такие вещи, чтобы гарантировать безопасность во время выполнения. Но это имеет последствия. Например, рассмотрим метод addAll() из интерфейса Collection. Каков его синтаксис? Интуитивно вы бы написали его так:
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
Но тогда вы не сможете сделать следующее (что совершенно безопасно):
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!! Would not compile with the naive declaration of addAll:
// Collection<String> is not a subtype of Collection<Object>
}
(В Java вы, вероятно, узнали это нелегко, см. Effective Java, 3-е издание, пункт 28: Предпочитайте списки массивам)
Вот почему фактический синтаксис addAll() выглядит следующим образом:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
Аргумент типа подстановки ? extends E указывает, что этот метод принимает коллекцию объектов типа E или подтипа E, а не только E самого. Это означает, что вы можете безопасно читать элементы из этой коллекции (элементы этой коллекции являются экземплярами подкласса E), но не можете записывать в неё, так как вы не знаете, какие объекты соответствуют этому неизвестному подтипу E. В обмен на это ограничение вы получаете желаемое поведение: Collection<String> является подтипом Collection<? extends Object>. Другими словами, подстановка с ограничением extends (верхнее ограничение) делает тип ковариантным.
Ключ к пониманию того, почему это работает, довольно прост: если вы можете только извлекать элементы из коллекции, то использование коллекции String и чтение Object из неё в порядке вещей. И наоборот, если вы можете только добавлять элементы в коллекцию, то использование коллекции Object и добавление String в неё в порядке вещей: в Java есть List<? super String>, надтип List<Object>.
Последнее называется противовариантностью, и вы можете вызывать только методы, принимающие String в качестве аргумента на List<? super String> (например, вы можете вызвать add(String) или set(int, String)). Если вы вызываете что-то, что возвращает T в List<T>, вы не получите String, а Object.
Джошуа Блок дал название производителям объектам, с которыми вы только работаете, и потребителям тем, с которыми вы только работаете. Он рекомендует:
Изменчивость на уровне объявления
Предположим, что есть обобщённый интерфейс Source<T> без методов, принимающих T в качестве параметра, только методов, возвращающих T:
// Java
interface Source<T> {
T nextT();
}
Тогда было бы совершенно безопасно хранить ссылку на экземпляр Source<String> в переменной типа Source<Object> — нет методов-потребителей для вызова. Но Java этого не знает и по-прежнему запрещает это:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// ...
}
Для исправления этого вы должны объявить объекты типа Source<? extends Object>. Делать это бессмысленно, потому что вы можете вызывать те же самые методы на такой переменной, как и раньше, так что нет никакой дополнительной пользы от более сложного типа. Но компилятор этого не знает.
В Kotlin есть способ объяснить компилятору подобные вещи. Это называется изменчивостью на уровне объявления: вы можете добавить аннотацию к параметру типа T для Source , чтобы убедиться, что он используется только для возвращения (производства) из членов Source<T>, а никогда не потребляется. Для этого используйте модификатор out:
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// ...
}
Общее правило таково: когда параметр типа T класса C объявлен как out, он может встречаться только в позиции out в членах C, но в обмен на это C<Base> может безопасно быть надтипом C<Derived>.
Другими словами, можно сказать, что класс C является ковариантным по параметру T, или что T — это ковариантный параметр типа. Можно рассматривать C как производителя T и НЕ как потребителя T.
Модификатор out называется аннотацией изменчивости, и поскольку он указан на месте объявления параметра типа, он обеспечивает изменчивость на уровне объявления. Это в отличие от изменчивости на уровне использования Java, где подстановки в использовании типов делают типы ковариантными.
В дополнение к out, Kotlin предоставляет дополнительную аннотацию изменчивости: in. Она делает параметр типа противовариантным, то есть он может только потребляться, а не производиться. Хороший пример противовариантного типа — Comparable:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, you can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}
Слова in и out, кажется, понятны сами по себе (поскольку они уже успешно использовались в C# довольно долго), и поэтому вышеупомянутая мнемоника на самом деле не нужна. Ее, фактически, можно перефразировать на более высоком уровне абстракции:
Сущностное Преобразование: Потребитель in, Производитель out!:-)
Проекции типов
Вариативность в месте использования: проекции типов
Очень легко объявить параметр типа T как out и избежать проблем с подтипированием в месте использования, но некоторые классы не могут фактически быть ограничены только возвращением T! Хорошим примером этого является Array:
class Array<T>(val size: Int) {
operator fun get(index: Int): T { ... }
operator fun set(index: Int, value: T) { ... }
}
Этот класс не может быть ни ковариантным, ни контравариантным по отношению к T. И это накладывает определенные ограничения. Рассмотрим следующую функцию:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
Эта функция должна копировать элементы из одного массива в другой. Попробуем применить её на практике:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// ^ type is Array<Int> but Array<Any> was expected
Здесь вы сталкиваетесь с той же знакомой проблемой: Array<T> является инвариантным по отношению к T, и поэтому ни Array<Int>, ни Array<Any> не являются подтипом друг друга. Почему? Опять же, это происходит потому, что copy может иметь неожиданное поведение, например, оно может попытаться записать String в from, и если вы фактически передадите массив Int, то позже будет брошено исключение ClassCastException.
Чтобы запретить функции copy записывать в from, можно сделать следующее:
fun copy(from: Array<out Any>, to: Array<Any>) { ... }
Это проекция типа, что означает, что from не является простым массивом, а является ограниченным (проецированным) массивом. Вы можете вызывать только методы, которые возвращают параметр типа T, что в данном случае означает, что вы можете вызывать только get(). Это наш подход к вариативности в месте использования, и он соответствует Array<? extends Object> Java, будучи немного проще.
Вы можете спроецировать тип с помощью in:
fun fill(dest: Array<in String>, value: String) { ... }
Array<in String> соответствует Array<? super String> Java. Это означает, что вы можете передать массив CharSequence или массив Object в функцию fill().
Проекции звёзд
Иногда вы хотите указать, что вы ничего не знаете о типе аргумента, но всё ещё хотите использовать его безопасным способом. Безопасный способ здесь заключается в определении такой проекции обобщённого типа, что любая конкретная реализация этого обобщённого типа будет подтипом этой проекции.
Kotlin предоставляет так называемый синтаксис проекции звёзд для этого:
Для
Foo<out T : TUpper>, гдеT— ковариантный параметр типа с верхним ограничениемTUpper,Foo<*>эквивалентноFoo<out TUpper>. Это означает, что когдаTнеизвестен, вы можете безопасно считывать значенияTUpperизFoo<*>.Для
Foo<in T>, гдеT— контравариантный параметр типа,Foo<*>эквивалентноFoo<in Nothing>. Это означает, что вы не можете безопасно записывать вFoo<*>способом, когдаTнеизвестен.Для
Foo<T : TUpper>, гдеT— инвариантный параметр типа с верхним ограничениемTUpper,Foo<*>эквивалентноFoo<out TUpper>для чтения значений иFoo<in Nothing>для записи значений.
Если обобщённый тип имеет несколько параметров типа, каждый из них может быть проецирован независимо. Например, если тип объявлен как interface Function<in T, out U>, вы можете использовать следующие проекции звёзд:
Function<*, String>означаетFunction<in Nothing, String>.Function<Int, *>означаетFunction<Int, out Any?>.Function<*, *>означаетFunction<in Nothing, out Any?>.
Обобщённые функции
Классы не единственные объявления, которые могут иметь параметры типа. Функции тоже могут. Параметры типа располагаются перед именем функции:
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString(): String { // extension function
// ...
}
Для вызова обобщённой функции укажите аргументы типа в месте вызова после имени функции:
val l = singletonList<Int>(1)
Аргументы типа можно опустить, если они могут быть выведены из контекста, поэтому следующий пример тоже работает:
val l = singletonList(1)
Обобщённые ограничения
Множество всех возможных типов, которые могут быть заменены заданным параметром типа, может быть ограничено обобщёнными ограничениями.
Верхние границы
Наиболее распространённым типом ограничения является верхняя граница, которая соответствует ключевому слову extends Java:
fun <T : Comparable<T>> sort(list: List<T>) { ... }
Тип, указанный после двоеточия, является верхней границей, указывающей, что только подтип Comparable<T> может быть заменён на T. Например:
sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable<Int> sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>
Умолчательной верхней границей (если она не была указана) является Any?. В угловых скобках может быть указана только одна верхняя граница. Если для одного и того же параметра типа требуется более одной верхней границы, необходимо использовать отдельную where-определяющую часть:
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
Переданный тип должен одновременно удовлетворять всем условиям в where части. В приведенном выше примере тип T должен реализовывать как CharSequence, так и Comparable.
Стирание типов
Проверка типа безопасности, которую выполняет Kotlin для использования обобщённых объявлений, выполняется во время компиляции. Во время выполнения экземпляры обобщённых типов не содержат никакой информации об их фактических аргументах типа. Считается, что информация о типе стирается. Например, экземпляры Foo<Bar> и Foo<Baz?> стираются до просто Foo<*>.
Проверки и преобразования типов обобщённых типов
Из-за стирания типов нет универсального способа проверить во время выполнения, был ли экземпляр обобщённого типа создан с определёнными аргументами типа, и компилятор запрещает такие проверки типа, как is—проверки, такие как ints is List<Int> или list is T (параметр типа). Однако вы можете проверить экземпляр на соответствие звёздно-проецированному типу:
if (something is List<*>) {
something.forEach { println(it) } // The items are typed as `Any?`
}
Аналогично, когда у вас уже есть аргументы типа экземпляра, проверенные статически (во время компиляции), вы можете выполнить проверку is или преобразование, которое затрагивает необобщённую часть типа. Обратите внимание, что угловые скобки в этом случае опущены:
fun handleStrings(list: MutableList<String>) {
if (list is ArrayList) {
// `list` is smart-cast to `ArrayList<String>`
}
}
Тот же синтаксис, но с опущенными аргументами типа, может использоваться для преобразований, которые не учитывают аргументы типа: list as ArrayList.
Аргументы типа вызовов обобщённых функций также проверяются только во время компиляции. Внутри тел функций параметры типа не могут использоваться для проверок типа, и преобразования типа к параметрам типа (foo as T) не проверяются. Единственным исключением являются встроенные функции с реализованными параметрами типа, у которых фактические аргументы типа вставляются в каждом месте вызова. Это позволяет проверять типы и выполнять преобразования для параметров типа. Однако описанные выше ограничения всё ещё применяются к экземплярам обобщённых типов, используемым внутри проверок или преобразований. Например, в проверке типа arg is T, если arg является экземпляром обобщённого типа, его аргументы типа всё равно стираются.
//sampleStart
inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
if (first !is A || second !is B) return null
return first as A to second as B
}
val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)
val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // Compiles but breaks type safety!
// Expand the sample for more details
//sampleEnd
fun main() {
println("stringToSomething = " + stringToSomething)
println("stringToInt = " + stringToInt)
println("stringToList = " + stringToList)
println("stringToStringList = " + stringToStringList)
//println(stringToStringList?.second?.forEach() {it.length}) // This will throw ClassCastException as list items are not String
}
Преобразования типов без проверки
Преобразования типов к обобщённым типам с конкретными аргументами типа, такими как foo as List<String>, не могут быть проверены во время выполнения.
Эти преобразования типов без проверки могут использоваться, когда безопасность типа подразумевается логикой программы высокого уровня, но не может быть выведена непосредственно компилятором. См. пример ниже.
fun readDictionary(file: File): Map<String, *> = file.inputStream().use {
TODO("Read a mapping of strings to arbitrary elements.")
}
// We saved a map with `Int`s into this file
val intsFile = File("ints.dictionary")
// Warning: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>
Для преобразования в последней строке появляется предупреждение. Компилятор не может полностью проверить его во время выполнения и не даёт гарантии, что значения в карте являются Int.
Чтобы избежать преобразований типов без проверки, можно переработать структуру программы. В приведённом выше примере вы могли бы использовать интерфейсы DictionaryReader<T> и DictionaryWriter<T> с безопасными по типу реализациями для разных типов. Можно ввести разумные абстракции, чтобы перенести преобразования типов без проверки из места вызова в детали реализации. Правильное использование вариативности обобщённых типов также может помочь.
Для обобщённых функций использование реализованных параметров типа делает преобразования типа, такие как arg as T, проверяемыми, если тип arg не имеет своих собственных аргументов типа, которые стираются.
Предупреждение о преобразовании типа без проверки можно подавить, пометив оператор или объявление, где оно возникает, аннотацией @Suppress("UNCHECKED_CAST"):
inline fun <reified T> List<*>.asListOfType(): List<T>? =
if (all { it is T })
@Suppress("UNCHECKED_CAST")
this as List<T> else
null
Оператор подчеркивания для аргументов типов
Оператор подчеркивания _ может использоваться для аргументов типов. Используйте его, чтобы автоматически определить тип аргумента, когда другие типы явно указаны:
abstract class SomeClass<T> {
abstract fun execute() : T
}
class SomeImplementation : SomeClass<String>() {
override fun execute(): String = "Test"
}
class OtherImplementation : SomeClass<Int>() {
override fun execute(): Int = 42
}
object Runner {
inline fun <reified S: SomeClass<T>, T> run() : T {
return S::class.java.getDeclaredConstructor().newInstance().execute()
}
}
fun main() {
// T is inferred as String because SomeImplementation derives from SomeClass<String>
val s = Runner.run<SomeImplementation, _>()
assert(s == "Test")
// T is inferred as Int because OtherImplementation derives from SomeClass<Int>
val n = Runner.run<OtherImplementation, _>()
assert(n == 42)
}
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/generics.html