Обобщения: 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), но не можете записывать в него, так как вы не знаете, какие объекты соответствуют этому неизвестному подтипу 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>. Это означает, что когдаTнеизвестен, нет безопасного способа записывать вFoo<*>.Для
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-проверки.
Приведения типа к обобщённым типам с конкретными аргументами типа, например, foo as List<String>, не могут быть проверены во время выполнения. Эти непроверенные приведения могут использоваться, когда безопасность типа подразумевается логикой программы высокого уровня, но не может быть выведена непосредственно компилятором. Компилятор выдаёт предупреждение о непроверенных приведениях, а во время выполнения проверяется только необобщённая часть (эквивалентно foo as List<*>).
Аргументы типа вызовов обобщённых функций также проверяются только во время компиляции. Внутри функций тела параметры типа не могут использоваться для проверок типа, а приведения типа к параметрам типа (foo as T) являются непроверенными. Однако реализованные параметры типа встроенных функций подставляются фактическими аргументами типа в теле встроенной функции на местах вызова, поэтому они могут использоваться для проверок типа и приведений, с теми же ограничениями для экземпляров обобщённых типов, что описаны выше.
© 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