Spec-Zone.ru › Kotlin 1.4

Обобщения

Как и в Java, классы в Kotlin могут иметь параметры типа:

class Box<T>(t: T) {
    var value = t
}

В общем случае, для создания экземпляра такого класса, нам нужно указать аргументы типа:

val box: Box<Int> = Box<Int>(1)

Но если параметры могут быть выведены, например, из аргументов конструктора или каким-либо другим способом, можно опустить аргументы типа:

val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking about Box<Int>

Изменчивость

Одна из самых сложных частей системы типов Java — это символы-подстановочные знаки (см. Java Generics FAQ). А в Kotlin их нет. Вместо этого есть два других понятия: изменчивость на уровне объявления и проекции типов.

Сначала давайте подумаем, почему Java нуждается в этих загадочных символах-подстановочных знаках. Проблема объяснена в Effective Java, 3-е издание, пункт 31: «Используйте ограниченные подстановочные знаки для повышения гибкости API». Во-первых, типы обобщений в Java являются инвариантными, что означает, что List<String> не является подтипом List<Object>. Почему так? Если бы List не был инвариантным, он был бы не лучше, чем массивы Java, так как следующий код компилировался бы и приводил к исключению во время выполнения:

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! A compile-time error here saves us from a runtime exception later
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to 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);
  // !!! Would not compile with the naive declaration of addAll:
  // Collection<String> is not a subtype of Collection<Object>
}

(В Java мы усвоили этот урок с трудом, см. Effective Java, 3-е издание, пункт 28: «Предпочитайте списки массивам»)

Вот почему фактическая подпись 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 (верхним ограничением) делает тип ковариантным.

Ключ к пониманию того, почему этот трюк работает, довольно прост: если вы можете только брать элементы из коллекции, то использование коллекции String и чтение Object из неё — это нормально. И наоборот, если вы можете только вставлять элементы в коллекцию, то использование коллекции Object и вставка String в неё — это нормально: в Java у нас есть List<? super String> — надтип List<Object>.

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

Джошуа Блок называет объекты, из которых вы только читаете, производителями, а те, в которые вы только записываете, — потребителями. Он рекомендует: «Для максимальной гибкости используйте подстановочные знаки типов для входных параметров, которые представляют производителей или потребителей», и предлагает следующую мнемонику:

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; // !!! Not allowed in 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 // This is OK, since T is an out-parameter
    // ...
}

Общее правило таково: когда параметр типа T класса C объявлен out, он может встречаться только в out-позиции в членах C, но в обмен на это C<Base> может безопасно быть надтипом C<Derived>.

В «умных словах» говорят, что класс C ковариантный по параметру T, или что T — ковариантный параметр типа. Можно считать, что C является производителем T и не является потребителем 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 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

Мы считаем, что слова in и out понятны сами по себе (так как они уже довольно долго используются в C#), поэтому упомянутая выше мнемоника не очень нужна, и её можно перефразировать для более высокой цели:

Сущностная Трансформация: Потребитель в, Производитель из! :-)

Проекции типов

Изменчивость на месте использования: Проекции типов

Очень удобно объявить параметр типа T как out и избежать проблем с подтипированием на месте использования, но некоторые классы не могут быть ограничены только возвращением T! Хорошим примером этого является массив:

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)
//   ^ type is Array<Int> but Array<Any> was expected

Здесь мы сталкиваемся с той же знакомой проблемой: Array<T> инвариантный в T, поэтому ни Array<Int>, ни Array<Any> не являются подтипом друг друга. Почему? Опять же, потому что copy может делать плохие вещи, т.е. может попытаться записать, скажем, строку в 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 предоставляет так называемый синтаксис звёздной проекции для этого:

  • Для 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?>.

Примечание: звёздные проекции очень похожи на необработанные типы Java, но безопасны.

Обобщённые функции

Не только классы могут иметь параметры типа. Функции тоже могут. Параметры типа помещаются перед именем функции:

fun <T> singletonList(item: T): List<T> {
    // ...
}

fun <T> T.basicToString(): String {  // extension function
    // ...
}

Чтобы вызвать обобщённую функцию, укажите аргументы типа в месте вызова после имени функции:

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)) // OK. Int is a subtype of Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of 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) являются непроверенными. Однако реализованные параметры типа инлайновых функций подставляются фактическими аргументами типа в теле вставленной функции в местах вызова, и таким образом могут использоваться для проверок и приведений типов, при тех же ограничениях для экземпляров обобщённых типов, что описаны выше.

© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/generics.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API