Spec-Zone.ru › Kotlin 1.8

Обобщения: in, out, where

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

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 it is Box<Int>

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

Один из самых сложных аспектов системы типов Java — это типы с подстановочными знаками (см. Java Generics FAQ). В Kotlin таких нет. Вместо этого 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); // 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>. Другими словами, подстановочный знак с ограничением расширения (верхняя граница) делает тип ковариантным.

Ключ к пониманию того, почему это работает, довольно прост: если вы можете только брать элементы из коллекции, то использование коллекции 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, you can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

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

Сущностное преобразование: потребитель в, производитель out!:-)

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

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

Очень легко объявить параметр типа T как out и избежать проблем с подтипированием на месте использования, но некоторые классы не могут фактически быть ограничены только возвращением 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)
//   ^ type is Array<Int> but Array<Any> was expected

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

  • Для 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 — такие, как ints is List<Int> или list is T (параметр типа). Однако вы можете проверить экземпляр на соответствие звёздному спроецированному типу:

if (something is List<*>) {
    something.forEach { println(it) } // The items are typed as `Any?`
}

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

fun handleStrings(list: MutableList<String>) {
    if (list is ArrayList) {
        // `list` is smart-cast to `ArrayList<String>`
    }
}

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

Аргументы типа вызовов общих функций также проверяются только во время компиляции. Внутри тел функций параметры типа не могут быть использованы для проверок типа, а преобразования типа в параметры типа (foo as T) не проверяются. Единственным исключением являются встроенные функции с параметрами типа reified, у которых фактические аргументы типа встроены в каждом месте вызова. Это позволяет выполнять проверки типа и преобразования для параметров типа. Однако вышеописанные ограничения всё ещё применяются к экземплярам общих типов, используемых внутри проверок или преобразований. Например, в проверке типа 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>>() // Compiles but breaks type safety!
// Expand the sample for more details

//sampleEnd

fun main() {
    println("stringToSomething = " + stringToSomething)
    println("stringToInt = " + stringToInt)
    println("stringToList = " + stringToList)
    println("stringToStringList = " + stringToStringList)
    //println(stringToStringList?.second?.forEach() {it.length}) // This will throw ClassCastException as list items are not String
}

Непроверенные преобразования

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

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

// We saved a map with `Int`s into this file
val intsFile = File("ints.dictionary")

// Warning: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>

Для преобразования в последней строке появляется предупреждение. Компилятор не может полностью проверить его во время выполнения и не даёт никаких гарантий, что значения в отображении являются Int.

Чтобы избежать непроверенных преобразований, можно перепроектировать структуру программы. В приведённом выше примере вы могли бы использовать интерфейсы DictionaryReader<T> и DictionaryWriter<T> с безопасными по типу реализациями для разных типов. Вы можете ввести разумные абстракции, чтобы перенести непроверенные преобразования из места вызова в детали реализации. Правильное использование изменчивости общих типов также может помочь.

Для общих функций использование параметров типа reified делает преобразования, такие как 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>) сохраняют информацию об отброшенном типе их элементов, и преобразования типа в тип массива частично проверяются: нулевые значения и фактические аргументы типа элемента всё ещё стираются. Например, преобразование foo as Array<List<String>?> будет успешным, если foo является массивом, содержащим любые List<*>, независимо от того, является ли он nullable или нет.

END_OF_DOCUMENT_MARKER

Оператор подчеркивания для аргументов типов

Оператор подчеркивания _ может использоваться для аргументов типов. Используйте его, чтобы автоматически определить тип аргумента, когда другие типы явно указаны:

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 is inferred as String because SomeImplementation derives from SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")

    // T is inferred as Int because OtherImplementation derives from SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}
Последнее изменение: 10 января 2023
Закрытые классы Вложенные и внутренние классы

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

Spec-Zone.ru

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