Обобщения (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

Давайте подумаем, зачем Java нужны эти загадочные маски. Прежде всего, обобщённые типы в Java являются инвариантными (ориг. invariant). Это означает, что List<String> не является подтипом List<Object>. Если бы List не был инвариантным, он был бы ничем не лучше массивов Java: следующий код компилировался бы, но вызывал исключение во время выполнения.

// Java
List<String> strs = new ArrayList<String>();

// Java сообщает о несоответствии типов здесь, во время компиляции.
List<Object> objs = strs;

// Что было бы, если бы Java этого не делала?
// Мы могли бы положить Integer в список строк.
objs.add(1);

// А затем во время выполнения Java выбросила бы
// ClassCastException: Integer cannot be cast to String
String s = strs.get(0);

Java запрещает подобные вещи, гарантируя тем самым безопасность выполнения кода. Но у такого подхода есть свои последствия. Рассмотрим, например, метод addAll() интерфейса Collection. Какова сигнатура данного метода? Интуитивно мы бы указали её таким образом:

// Java
interface Collection<E> ... {
    void addAll(Collection<E> items);
}

Но тогда мы бы не могли выполнять следующую простую операцию (которая является абсолютно безопасной):

// Java

// Следующий код не скомпилировался бы с наивным объявлением addAll:
// Collection<String> не является подтипом Collection<Object>
void copyAll(Collection<Object> to, Collection<String> from) {
    to.addAll(from);
}

Вот почему сигнатура 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>, который принимает String или любой его супертип.

Это называется контрвариантностью (ориг.: contravariance). Вы можете вызвать только те методы, которые принимают String в качестве аргумента в List<? super String> (например, вы можете вызвать add(String) или set(int, String)). В случае, если вы вызываете из List<T> что-то с возвращаемым значением T, вы получаете не String, а Object.

Джошуа Блох (Joshua Bloch) хорошо объясняет эту проблему в книге Effective Java, 3rd Edition (Item 31: “Use bounded wildcards to increase API flexibility”). Он называет объекты: - Производителями (ориг.: 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, то есть мы можем передать массив String, 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.

Определённо ненулевые типы

Чтобы упростить взаимодействие с обобщёнными Java-классами и интерфейсами, Kotlin поддерживает объявление обобщённого параметра типа как определённо ненулевого.

Чтобы объявить обобщённый тип T определённо ненулевым, укажите его с & Any. Например: T & Any.

У определённо ненулевого типа должна быть верхняя граница, допускающая null.

Чаще всего определённо ненулевые типы объявляют, когда нужно переопределить Java-метод, аргумент которого содержит @NotNull. Например, рассмотрим метод load():

import org.jetbrains.annotations.*;

public interface Game<T> {
    public T save(T x) {}
    @NotNull
    public T load(@NotNull T x) {}
}

Чтобы успешно переопределить метод load() в Kotlin, нужно объявить T1 как определённо ненулевой:

interface ArcadeGame<T1> : Game<T1> {
    override fun save(x: T1): T1
    // T1 является определённо ненулевым
    override fun load(x: T1 & Any): T1 & Any
}

Если вы работаете только с Kotlin, вам вряд ли потребуется объявлять определённо ненулевые типы явно: за это отвечает вывод типов Kotlin.

Стирание типов

Проверки безопасности типов, которые Kotlin выполняет при использовании обобщённых объявлений, происходят во время компиляции. Во время выполнения экземпляры обобщённых типов не содержат информации о своих фактических аргументах типа. Говорят, что информация о типе стирается. Например, экземпляры Foo<Bar> и Foo<Baz?> стираются просто до Foo<*>.

Проверки и приведения обобщённых типов

Из-за стирания типов нет общего способа во время выполнения проверить, был ли экземпляр обобщённого типа создан с определёнными аргументами типа. Поэтому компилятор запрещает такие is-проверки, как ints is List<Int> или list is T (параметр типа). Однако можно проверить экземпляр на соответствие типу со звёздной проекцией:

if (something is List<*>) {
    something.forEach { println(it) } // Элементы имеют тип `Any?`
}

Аналогично, если аргументы типа экземпляра уже проверены статически (во время компиляции), можно выполнить is-проверку или приведение, которое затрагивает необобщённую часть типа. Обратите внимание, что угловые скобки в этом случае опускаются:

fun handleStrings(list: MutableList<String>) {
    if (list is ArrayList) {
        // `list` умно приведён к `ArrayList<String>`
    }
}

Тот же синтаксис с опущенными аргументами типа можно использовать для приведений, которые не учитывают аргументы типа: list as ArrayList.

Аргументы типа при вызове обобщённых функций также проверяются только во время компиляции. Внутри тел функций параметры типа нельзя использовать для проверок типов, а приведения к параметрам типа (foo as T) являются непроверенными. Единственное исключение — inline-функции с овеществлёнными параметрами типа: их фактические аргументы типа встраиваются в каждом месте вызова. Это позволяет выполнять проверки типов и приведения для параметров типа. Однако для экземпляров обобщённых типов, используемых в проверках или приведениях, всё равно действуют описанные выше ограничения. Например, в проверке типа 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>>() // Компилируется, но нарушает безопасность типов!
// Подробности см. в main ниже

//sampleEnd

fun main() {
    println("stringToSomething = " + stringToSomething)
    println("stringToInt = " + stringToInt)
    println("stringToList = " + stringToList)
    println("stringToStringList = " + stringToStringList)
    //println(stringToStringList?.second?.forEach() {it.length}) // Выбросит ClassCastException, так как элементы списка не String
}

Непроверенные приведения

Приведения к обобщённым типам с конкретными аргументами типа, например foo as List<String>, нельзя проверить во время выполнения. Такие непроверенные приведения можно использовать, когда безопасность типов следует из высокоуровневой логики программы, но компилятор не может вывести её напрямую. См. пример ниже.

fun readDictionary(file: File): Map<String, *> = file.inputStream().use {
    TODO("Read a mapping of strings to arbitrary elements.")
}

// В этот файл мы сохранили отображение с `Int`
val intsFile = File("ints.dictionary")

// Предупреждение: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>

Для приведения в последней строке появляется предупреждение. Компилятор не может полностью проверить его во время выполнения и не гарантирует, что значения в map имеют тип 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

На JVM: типы массивов (Array<Foo>) сохраняют информацию о стёртом типе своих элементов, и приведения к типу массива проверяются частично: допустимость null и фактические аргументы типа элемента всё равно стираются. Например, приведение foo as Array<List<String>?> завершится успешно, если foo — массив, содержащий любой List<*>, независимо от того, допускает он 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 выводится как String, потому что SomeImplementation наследуется от SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")

    // T выводится как Int, потому что OtherImplementation наследуется от SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}