Обобщения: 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-обобщениям). В 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), но не можете записывать в него, так как вы не знаете, какие объекты соответствуют этому неизвестному подтипу 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(). Это наш подход к вариативности на месте использования, и он соответствует вариативности Java Array<? extends Object>, будучи немного проще.
Вы можете спроецировать тип с помощью in также:
fun fill(dest: Array<in String>, value: String) { ... }
Array<in String> соответствует вариативности Java Array<? super String>. Это означает, что вы можете передать массив 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)
Ограничения обобщённых типов
Множество всех возможных типов, которые могут быть заменены на данный параметр типа, может быть ограничено ограничениями обобщённых типов.
Верхние границы
Наиболее распространённым типом ограничения является верхняя граница, которая соответствует ключевому слову Java extends:
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–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/generics.html