Котлин: дженерики и дисперсия

Я хочу создать функцию расширения на Throwable которая, учитывая KClass , рекурсивно ищет корневую причину, которая соответствует аргументу. Ниже приводится одна попытка, которая работает:

 fun <T : Throwable> Throwable.getCauseIfAssignableFrom(e: KClass<T>): Throwable? = when { this::class.java.isAssignableFrom(e.java) -> this nonNull(this.cause) -> this.cause?.getCauseIfAssignableFrom(e) else -> null } 

Это тоже работает:

 fun Throwable.getCauseIfAssignableFrom(e: KClass<out Throwable>): Throwable? = when { this::class.java.isAssignableFrom(e.java) -> this nonNull(this.cause) -> this.cause?.getCauseIfAssignableFrom(e) else -> null } 

Я вызываю функцию следующим образом: e.getCauseIfAssignableFrom(NoRemoteRepositoryException::class) .

Тем не менее, Kotlin сообщает о дженериках:

Это называется дисперсией объявления-сайта: мы можем аннотировать параметр типа T источника, чтобы убедиться, что он возвращается только от участников Source и никогда не потребляется. Для этого мы предоставляем модификатор out

 abstract class Source<out T> { abstract fun nextT(): T } fun demo(strs: Source<String>) { val objects: Source<Any> = strs // This is OK, since T is an out-parameter // ... } 

В моем случае параметр e не возвращается, а потребляется. Мне кажется, что он должен быть объявлен как e: KClass<in Throwable> но он не компилируется. Однако, если я думаю о том out что «вы можете только читать или возвращать его», а «как вы можете писать или присваивать ему значение», тогда это имеет смысл. Может кто-нибудь объяснить?

В вашем случае вы фактически не используете дисперсию параметра типа: вы никогда не передаете значение или не используете значение, возвращаемое при вызове на ваш e: KClass<T> .

Дисперсия описывает, какие значения вы можете передать в качестве аргумента и что вы можете ожидать от значений, возвращаемых свойствами и функциями при работе с прогнозируемым типом (например, внутри реализации функции). Например, если KClass<T> вернет T (как написано в сигнатуре), KClass<out SomeType> может вернуть либо SomeType либо любой из его подтипов . Напротив, если KClass<T> ожидает аргумент T , KClass<in SomeType> ожидает некоторые из супертипов SomeType (но точно неизвестно, что именно).

Фактически это определяет ограничения на фактические аргументы типа экземпляров, которые вы передаете такой функции. С инвариантным типом KClass<Base> вы не можете передать KClass<Super> или KClass<Derived> (где Derived : Base : Super ). Но если функция ожидает KClass<out Base> , вы также можете передать KClass<Derived> , потому что она удовлетворяет вышеупомянутому требованию: он возвращает Derived из своих методов, которые должны возвращать Base или его подтип (но это не так для KClass<Super> ). И, напротив, функция, которая ожидает, что KClass<in Base> может также получить KClass<Super>

Итак, когда вы переписываете getCauseIfAssignableFrom для принятия e: KClass<in Throwable> , вы указываете, что в реализации вы хотите передать Throwable некоторой общей функции или свойству e , и вам нужен экземпляр KClass который способный справиться с этим. Класс Any::class или Throwable::class подходит, но это не то, что вам нужно.

Поскольку вы не вызываете какие-либо функции e и не KClass<*> доступа к каким-либо его свойствам, вы даже можете сделать свой тип KClass<*> (четко заявив, что вам все равно, что это за тип и разрешить ничего), и это сработает.

Но ваш случай использования требует, чтобы вы ограничивали тип подтипом Throwable . Здесь работает KClass<out Throwable> : он ограничивает аргумент типа подтипом Throwable (опять же, вы утверждаете, что для функций и свойств KClass<T> которые возвращают T или что-то с T like Function<T> , вы хотите использовать возвращаемое значение, как если бы T был подтипом Throwable , хотя вы этого не делаете).

Другой вариант, который работает для вас, – это определение верхней границы <T : Throwable> . Это похоже на <out Throwable> , но дополнительно захватывает аргумент типа KClass<T> и позволяет использовать его где-то еще в сигнатуре (в типе возвращаемого значения или типах других параметров) или внутри реализации.

В примере, который вы приводите из документации, вы показываете общий класс с аннотацией. Эта аннотация дает пользователям класса гарантию, что класс не будет выносить ничего, кроме T или класса, полученного из T.

В примере с кодом вы показываете общий параметр функции с аннотацией по типу параметра. Это дает пользователям параметра гарантию того, что параметр не будет ничем KClass<Throwable> как T (в вашем случае KClass<Throwable> ) или класс, полученный из T (a KClass<{derived from Throwable}> ).

Теперь измените мышление. Если вы должны использовать e: KClass<in Throwable> то вы ограничиваете параметр supers of Throwable .

В случае ошибки вашего компилятора, не имеет значения, использует ли ваша функция методы или свойства e . В вашем случае объявление параметра ограничивает способ вызова функции, а не как сама функция использует этот параметр. Поэтому, используя вместо вместо NorRemoteRepositoryException::class ваш вызов функции с параметром NorRemoteRepositoryException::class .

Конечно, ограничения также применяются в вашей функции, но эти ограничения никогда не выполняются, потому что e не используется таким образом.

В других ответах выяснилось, почему вам не нужно отклоняться на этом сайте.

FYI, API будет более полезен, если вы вернете возврат к ожидаемому типу,

 @Suppress("UNCHECKED_CAST") fun <T : Any> Throwable.getCauseIfInstance(e: KClass<T>): T? = when { e.java.isAssignableFrom(javaClass) -> this as T else -> cause?.getCauseIfInstance(e) } 

но это скорее Kotlin-like использовать тип reified.

 inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? = generateSequence(this) { it.cause }.filterIsInstance<T>().firstOrNull() 

Это фактически то же самое, что писать явный цикл, но короче.

 inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? { var current = this while (true) { when (current) { is T -> return current else -> current = current.cause ?: return null } } } 

И в отличие от оригинала, этот метод не требует kotlin-reflect .

(Я также изменил поведение с isAssignableFrom на is ( instanceof ), мне трудно представить, как оригинал мог быть полезен.)