Skip to content

Latest commit

 

History

History
372 lines (292 loc) · 31 KB

generics.md

File metadata and controls

372 lines (292 loc) · 31 KB
type layout category title url
doc
reference
Syntax
Обобщения (Generics)

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