Kotlin: generics y varianza

Quiero crear una function de extensión en Throwable que, dado un KClass , KClass recursiva una causa raíz que coincida con el argumento. El siguiente es un bash que funciona:

 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 } 

Esto también funciona:

 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 } 

Llamo a la function como tal: e.getCauseIfAssignableFrom(NoRemoteRepositoryException::class) .

Sin embargo, los documentos de Kotlin sobre generics dicen:

Esto se llama varianza del sitio de statement: podemos anotar el parámetro de tipo T de la Fuente para asegurarnos de que solo se devuelve (produce) de los miembros de la Fuente, y nunca se consume. Para hacer esto, proporcionamos el modificador de salida

 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 // ... } 

En mi caso, el parámetro e no se devuelve, sino que se consume. Me parece que debe declararse como e: KClass<in Throwable> pero eso no se comstack. Sin embargo, si lo considero como "solo se puede leer de, o devolverlo" y como "solo se puede escribir, o asignarle un valor", entonces tiene sentido. ¿Alguien puede explicar?

En su caso, en realidad no utiliza la varianza del parámetro tipo: nunca pasa un valor o usa un valor devuelto por una llamada a su e: KClass<T> .

La varianza describe qué valores puede pasar como argumento y qué puede esperar de los valores devueltos por las properties y funciones cuando trabaja con el tipo proyectado (por ejemplo, dentro de una implementación de function). Por ejemplo, cuando una KClass<T> devolvería una T (como está escrita en la firma), una KClass<out SomeType> puede devolver SomeType o cualquiera de sus subtypes . Por el contrario, cuando un KClass<T> esperaría un argumento de T , un KClass<in SomeType> espera algunos de los supertypes de SomeType (pero se desconoce cuál exactamente).

Esto, de hecho, define las limitaciones en los arguments de tipo reales de las instancias que pasa a dicha function. Con invariante tipo KClass<Base> , no puede pasar una KClass<Super> o KClass<Derived> (donde Derived : Base : Super ). Pero si una function espera KClass<out Base> , también puede pasar una KClass<Derived> , porque satisface el requisito antes mencionado: devuelve Derived de sus methods que deberían devolver Base o su subtipo (pero eso no es cierto para KClass<Super> ). Y, por el contrario, una function que espera KClass<in Base> también puede recibir KClass<Super>

Entonces, cuando reescribe getCauseIfAssignableFrom para aceptar un e: KClass<in Throwable> , usted e: KClass<in Throwable> que en la implementación desea pasar un Throwable a alguna function genérica o una propiedad de e , y necesita una instancia de KClass que sea capaz de manejar eso. La Any::class o Throwable::class cabría, pero eso no es lo que necesita.

Como no llama a ninguna de las funciones de e y no accede a ninguna de sus properties, incluso puede hacer que su tipo KClass<*> ( KClass<*> claramente que no le importa cuál es el tipo y permita que sea cualquier cosa), y funcionaría.

Pero su caso de uso requiere que restrinja el tipo para que sea un subtipo de Throwable . Aquí es donde funciona KClass<out Throwable> : restringe el argumento type para ser un subtipo de Throwable (de nuevo, usted KClass<T> que, para las funciones y properties de KClass<T> que devuelve T o algo con T like Function<T> , quiere usar el valor de retorno como si T un subtipo de Throwable , aunque no lo haga).

La otra opción que funciona para usted es definir un límite superior <T : Throwable> . Esto es similar a <out Throwable> , pero además captura el argumento de tipo de KClass<T> y le permite usarlo en otro lugar en la firma (en el tipo de devolución o en los types de los otros parameters) o dentro de la implementación.

En el ejemplo que cita de la documentation, está mostrando una class genérica con la anotación de out . Esta anotación brinda a los usuarios de la class una garantía de que la class no arrojará nada más que una T o class derivada de T.

En el ejemplo de código, está mostrando un parámetro de function genérico con una anotación de out en el tipo de parámetro. Esto le está dando a los usuarios del parámetro la garantía de que el parámetro no será otra cosa que una T (en su caso una KClass<Throwable> ) o una class derivada de T (una KClass<{derived from Throwable}> ).

Ahora invierte el pensamiento para in . Si e: KClass<in Throwable> que usar e: KClass<in Throwable> entonces restringe el parámetro a supers de Throwable .

En el caso de su error de compilation, no es el punto si su function usa methods o properties de e . En su caso, la statement del parámetro está limitando cómo se puede llamar a la function, no cómo la function usa el parámetro. Por lo tanto, el uso de in lugar de out descalifica su llamada a la function con el parámetro NorRemoteRepositoryException::class .

Por supuesto, las restricciones también se aplican dentro de su function, pero esas restricciones nunca se ejercen porque e no se usa de esa manera.

Las otras respuestas han abordado por qué no necesita varianza en este sitio de uso .

FYI, la API sería más útil si lanzas la devolución al tipo esperado,

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

pero es más parecido a Kotlin usar un tipo reificado.

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

Esto es efectivamente lo mismo que escribir un ciclo explícito, pero más corto.

 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 } } } 

Y a diferencia del original, este método no requiere kotlin-reflect .

(También cambié el comportamiento de isAssignableFrom a is ( instanceof ); me cuesta imaginar cómo podría haber sido útil el original).

  • propiedad de delegado kotlin, en un método get () cómo puedo acceder al valor?
  • ¿Podemos acceder al PropertyMetaData de cualquier propiedad en kotlin?
  • Sobrecarga de extensión de Kotlin para Android
  • Cómo almacenar BigDecimal en Firebase?
  • La testing de instrumento Android con Espresso falla: NoSuchMethodError get () en javax.inject.Provider
  • Kotlin: los generics reificados no parecen funcionar bien para comparaciones hash / iguales
  • La extensión sintética de Kotlin y varias incluyen el mismo layout
  • Quiero abrir files desde carpetas
  • ¿Tiene Kotlin una syntax para los literales de Map?
  • Verificando si la cadena está vacía en Kotlin
  • Enlace de Android Kotlin ImageView