Обобщения (Generics): 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>
что-то с возвращаемым значением 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
)
не проверено. Однако параметры вещественного типа встроенных функций
заменяются фактическими аргументами типа в теле встроенной функции на стороне вызовов и поэтому могут использоваться
для проверки и приведения типов с теми же ограничениями для экземпляров обобщенных типов, как описано выше.