type | layout | category | title | url |
---|---|---|---|---|
doc |
reference |
Syntax |
Обобщения (Generics) |
Как и в Java, в Kotlin классы тоже могут иметь generic типы:
class Box<T>(t: T) {
var value = t
}
Для того, чтобы создать объект такого класса, необходимо предоставить тип в качестве аргумента:
val box: Box<Int> = Box<Int>(1)
Но если параметры могут выведены из контекста (в аргументах конструктора или в некоторых других случаях), можно опустить указание типа:
val box = Box(1) // 1 имеет тип Int, поэтому компилятор отмечает для себя, что у переменной box тип — Box<Int>
Одним из самых сложных мест в системе типов Java являются маски (ориг. wildcards) (см. Java Generics FAQ).
А в Kotlin этого нет. Вместо этого, у нас есть две другие вещи: вариативность на уровне объявления и проекции типов.
Для начала давайте подумаем на тему, зачем Java нужны эти странные маски. Проблема описана в книге Effective Java, Item 28: Use bounded wildcards to increase API flexibility.
Обобщающие типы в Java, прежде всего, неизменны. Это значит, что List<String>
не является подтипом List<Object>
.
Почему так? Если бы List был изменяемым, единственно лучшим решением для следующей задачи был бы массив, потому что после компиляции данный код вызвал бы ошибку в рантайме:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! Причина вышеуказанной проблемы заключена здесь, Java запрещает так делать
objs.add(1); // Тут мы помещаем Integer в список String'ов
String s = strs.get(0); // !!! ClassCastException: не можем кастовать Integer к 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); // !!! Не скомпилируется с нативным объявлением метода addAll:
// Collection<String> не является подтипом Collection<Object>
}
(В Java нам этот урок дорого стоил, см. Effective Java, Item 25: Prefer lists to arrays)
Вот почему сигнатура addAll()
на самом деле такая:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
Маска для аргумента ? extends T
указвает на то, что это метод принимает коллекцию объектов некого типа T
, а не сам T
.
Это значит, что мы можем безопасно читать объекты типа T
из содержимого (элементы коллекции являются экземплярами подкласса T), но не можем их изменять, потому что не знаем, какие объекты соответствуют этому неизвестному типу T
.
Минуя это ограничение, мы достигаем желаемого результата: Collection<String>
является подтипом Collection<? extends Object>
.
Выражаясь более "умными словами", маска с extends-связкой (верхнее связывание) делает тип ковариантным (ориг. covariant).
Ключом к пониманию, почему этот трюк работает, является довольно простая мысль: использование коллекции String
'ов и чтение из неё Object
ов нормально только в случае, если вы берёте элементы из коллекции. Наоборот, если вы только вносите элементы в коллекцию, то нормально брать коллекцию Object
'ов и помещать в неё String
и: в Java есть List<? super String>
, супертип List<Object>
'a.
Это назвается контрвариантностью. В List<? super String>
вы можете вызвать только те методы, которые принимают String в качестве аргумента (например, add(String)
или set(int, String)
). В случае, если вы вызываете из List<T>
что-то c возвращаемым значением T
, вы получаете не String
, а Object
.
Джошуа Блок (Joshua Block) называет объекты:
- Производителями (ориг.:producers), если из которых вы только читаете
- Потребителями (ориг.: consumers), если вы только записываете в них Его рекомендация: "Для максимальной гибкости, используйте маски (ориг. wildcards) на входных параметрах, которые представляют производителей или потребителей"
PECS настаивает на Producer-Extends, Consumer-Super.
Примечание: если вы используете объект-производитель, предположим,
List<? extends Foo>
, вы не можете вызвать методыadd()
илиset()
этого объекта. Но это не значит, что объект является неизменяемым (immutable): ничто не мешает вам вызвать методclear()
для того, чтобы очистить список, так какclear()
не имеет аргументов. Единственное, что гарантируют маски — безопасность типов. Неизменяемость (ориг.: immutability) — совершенно другая история.
Допустим, у нас есть generic интерфейс 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; // !!! Запрещено в Java
// ...
}
Чтобы исправить это, нам нужно объявить объекты типа Source<? extends Object>
, что в каком-то роде бессмысленно, потому что мы можем вызывать у переменных только те методы, что и ранее, стало быть более сложный тип не добавляет значения. Но компилятор не знает этого.
В Kotlin существует способ объяснить вещь такого рода компилятору. Он называется вариантность на уровне объявления: мы можем пометить аннотацией параметризованный тип T
класса Source
, чтобы удостовериться, что он только возвращается (производится) членами Source<T>
, и никогда не потребляется. Чтобы сделать это, нам необходимо использовать модификатор out
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // Всё в порядке, т.к. T — out-параметр
// ...
}
Общее правило таково: когда параметр T
класса С
объявлен как out, он может использоваться только в out-местах в членах C
. Но зато, C<Base>
может быть родителем C<Derived>
, и это будет безопасно.
Говоря "умными словами", класс C
ковариантен в параметре T
; или: T
является ковариантным параметризованным типом.
Модификатор out называют вариативной аннотацией, и так как он указывается на месте объявления типа параметра, речь идёт о вариативности на месте объявления. Эта концепция противопоставлена вариативности на месте использования из Java, где маски при использовании типа делают типы ковариантными.
В дополнении к out, Kotlin предоставляет дополнительную вариативную аннотацию in. Она делает параметризованный тип контравариантным: он может только потребляться, но не может производиться. Comparable
является хорошим примером такого класса:
abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 имеет тип Double, расширяющий Number
// Таким образом, мы можем присвоить значение x переменной типа Comparable<Double>
val y: Comparable<Double> = x // OK!
}
Мы верим, что слова in и out говорят сами за себя (так как они довольно успешно используются в C# уже долгое время), таким образом, мнемоника, приведённая выше не так уж и нужна, и её можно перефразировать следущим образом:
Экзистенцианальная Трансформация: Consumer in, Producer out! :-)
Объявлять параметризованный тип T
как out очень удобно: при его использовании не будет никаких проблем с подтипами. И это действительно так в случае с классами, которые могут быть ограничены на только возвращение T
. А как быть с теми классами, которые ещё и принимают T
? Пример: класс Array
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
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) // Ошибка: ожидалось (Array<Any>, Array<Any>)
Здесь мы попадаем в уже знакомую нам проблему: Array<T>
инвариантен в T
, таким образом Array<Int>
не является подтипом Array<Any>
. Почему? Опять же, потому что копирование может сотворить плохие вещи, например может произойти попытка записать, скажем, значение типа 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 предоставляет так называемый star-projection синтаксис для этого:
- Для
Foo<out T>
, гдеT
— ковариантный параметризованный тип с верхней границейTUpper
,Foo<*>
является эквивалентомFoo<out TUpper>
. Это значит, что когдаT
неизвестен, вы можете безопасно читать значения типаTUpper
изFoo<*>
. - Для
Foo<in T>
, гдеT
— ковариантный параметризованный тип,Foo<*>
является эквивалентомFoo<in Nothing>
. Это значит, что вы не можете безопасно писать вFoo<*>
при неизвестномT
. - Для
Foo<T>
, где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?>
.
Примечаение: "звёздные" проекции очень похожи на сырые (raw) типы из Java, за тем исключением, что являются безопасными.
Функции, как и классы, могут иметь типовые параметры. Типовые параметры помещаются перед именем функции:
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString() : String { // функция-расширение
// ...
}
Для вызова обобщённой функции, укажите тип аргументов на месте вызова после имени функции:
val l = singletonList<Int>(1)
Набор всех возможных типов, которые могут быть переданы в качестве параметра может быть ограничен с помощью обобщённых ограничений.
Самый распространённый тип ограничений это верхняя граница, которая соответствует ключевому слову extends из Java:
fun <T : Comparable<T>> sort(list: List<T>) {
// ...
}
Тип, указанный после двоеточия является верхней границей: только подтип Comparable<T>
может быть передан в T
. Например:
sort(listOf(1, 2, 3)) // Всё в порядке. Int — подтип Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Ошибка: HashMap<Int, String> не является подтипом Comparable<HashMap<Int, String>>
По умолчанию (если не указана явно) верняя граница — Any?
. Только одна верхняя граница может быть указана в угловых скобках.
В случае, если один параметризованный тип требует больше чем одной верхней границы, нам нужно использовать разделяющее where-условие:
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}