type | layout | category | title | menuLabel | url |
---|---|---|---|---|---|
doc |
reference |
Syntax |
Обобщения (Generics): in, out, where |
Обобщения: in, out, where |
Как и в Java, в Kotlin классы могут иметь типовые параметры.
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 их нет. Вместо этого, в Kotlin есть вариативность на уровне объявления и проекции типов.
Давайте подумаем, зачем Java нужны эти загадочные маски. Проблема хорошо описана в книге
Effective Java,
Item 28: Use bounded wildcards to increase API flexibility.
Прежде всего, обобщённые типы в Java являются инвариантными (ориг. invariant). Это означает,
что List<String>
не является подтипом List<Object>
. Если бы List
был изменяемым,
он был бы ничем не лучше массива в Java, потому что после компиляции данный код вызвал бы ошибку во время выполнения.
// 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 E
указывает на то, что этот метод принимает коллекцию объектов E
или некоего типа унаследованного от E
, а не только сам E
. Это значит, что вы можете безопасно читать
объекты типа E
из содержимого (элементы коллекции являются экземплярами подкласса E), но не можете их присваивать,
потому что не знаете, какие объекты соответствуют этому неизвестному подтипу E
. Минуя это ограничение,
желаемый результат может быть достигнут: Collection<String>
является подтипом Collection<? extends Object>
.
Другими словами, маска с extends-связкой (верхнее связывание) делает тип ковариантным (ориг. covariant).
Ключом к пониманию, почему этот трюк работает, является довольно простая мысль: использование коллекции String
'ов и
чтение из неё Object
нормально только в случае, если вы только берёте элементы из коллекции. Наоборот, если вы
только вносите элементы в коллекцию, то нормально брать коллекцию Object
'ов и помещать в неё String
'и:
в Java есть List<? super String>
, супертип List<Object>
.
Это назвается контрвариантностью (ориг.: contravariance). Вы можете вызвать только те методы, которые принимают
String
в качестве аргумента в List<? super String>
(например, вы можете вызвать add(String)
илиset(int, String)
).
В случае, если вы вызываете из List<T>
что-то c возвращаемым значением T
, вы получаете не String
, а Object
.
Джошуа Блох (Joshua Bloch) называет объекты:
- Производителями (ориг.: producers), если вы только читаете из них,
- Потребителями (ориг.: consumers), если вы только записываете в них.
Он рекомендует:
Для максимальной гибкости используйте маски на входных параметрах, которые представляют производителей или потребителей", и предлагает следующую мнемонику:
PECS обозначает Producer-Extends, Consumer-Super.
Если вы используете объект-производитель, предположим,
List<? extends Foo>
, вы не можете вызвать методыadd()
илиset()
этого объекта. Но это не значит, что объект является неизменяемым: например, ничто не мешает вам вызвать методclear()
для того, чтобы очистить список, так какclear()
не имеет аргументов.Единственное, что гарантируют маски (или другие типы вариантности) — безопасность типов. Неизменяемость — совершенно другая история.
Допустим, у вас есть обобщённый интерфейс 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
.
interface Source<out T> {
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
является ковариантным типовым параметром.
C
производит экземпляры типа 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 имеет тип 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) {
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)
// ^ Ошибка: тип Array<Int>, а ожидалось 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 : 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?>
.
"Звёздные" проекции очень похожи на сырые (raw) типы из Java, за тем исключением того, что они безопасны.
Функции, как и классы, могут иметь типовые параметры. Типовые параметры помещаются перед именем функции.
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString(): String { // функция-расширение
// ...
}
Для вызова обобщённой функции, укажите тип аргументов на месте вызова после имени функции.
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)) // Всё в порядке. Int — подтип Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Ошибка: HashMap<Int, String> не является подтипом 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
)
не проверено. Однако параметры веществленного типа встроенных функций
заменяются фактическими аргументами типа в теле встроенной функции на стороне вызовов и поэтому могут использоваться
для проверки и приведения типов с теми же ограничениями для экземпляров обобщенных типов, как описано выше.