Tengo una pregunta sobre Kotlin y los genéricos. Tengo un error de compilación, que no entiendo. Tengo un ejemplo muy simplificado a continuación. Dado que las clases de datos de Kotlin no pueden heredar de otras clases de datos, hice mucho del diseño de mi clase con composición. Eso conduce a clases de datos anidados. En algunos puntos, uso genéricos para cambiar el tipo de parámetros que puedo pasar a un objeto. Necesito modificar los valores de estas clases de datos y debido a que son inmutables, uso la función de copia y coloco esas cosas en una clase Mutator.
class Mutator<out P : Params>(val form: Form<P>) { fun modifyName(name: String): Form<P> = when (form.params) { is DefaultParams -> form.copy(params = form.params.copy(name = name)) // compile error is ExtendedParams -> form.copy(params = form.params.copy(name = name)) // compile error else -> form // also.. why do I need that.. Params is sealed.. } } sealed interface Params data class DefaultParams(val name: String) : Params data class ExtendedParams(val name: String, val age: Int) : Params data class Form<out P : Params>(val params: P) fun main() { val form: Form<DefaultParams> = Form(DefaultParams("John")) val mutator: Mutator<DefaultParams> = Mutator(form) val newForm: Form<DefaultParams> = mutator.modifyName("Joe") val next_form: Form<ExtendedParams> = Form(ExtendedParams("John", 30)) val next_mutator: Mutator<ExtendedParams> = Mutator(next_form) val next_newForm: Form<ExtendedParams> = next_mutator.modifyName("Joe") }Recibo errores en las dos primeras ramas del bloque when.
Tipo de discrepancia. Requerido: P Encontrado: DefaultParams
Tipo de discrepancia. Requerido: P Encontrado: ExtendedParams
¿No deberían ser Params el límite superior de P y, por lo tanto, estar bien para encontrar DefaultParams o ExtendedParams ? Y también se deben sellar Params , pero necesito un bloque else en el cuándo.
Sugeriría que simplemente use un elenco sin marcar aquí:
is DefaultParams -> form.copy(params = form.params.copy(name = name) as P) is ExtendedParams -> form.copy(params = form.params.copy(name = name) as P) Tenga en cuenta que esta conversión sin marcar es muy segura, debido a la semántica del método de copy . copy no cambia el tipo de tiempo de ejecución de su receptor. Por lo tanto, no es más peligroso pasar los form.params copiados que pasar los form.params originales. Y el compilador le permite pasar el form.params original.params aquí. El compilador simplemente no es lo suficientemente inteligente como para darse cuenta de que, dado que form.params es un Params específico, P también debe ser ese Params específico.
La instrucción when no es exhaustiva porque el tipo de form.params no es Params , sino P . (Sí, el compilador es bastante estúpido a veces). De acuerdo con spec , una condición para una expresión when exhaustiva es:
La expresión enlazada es de una clase o interfaz sellada
Tenga en cuenta que no dice "un parámetro de tipo limitado a una clase o interfaz sellada". Entonces, si solo agrega as Params , será exhaustivo:
fun modifyName(name: String): Form<P> = (form.params as Params).let { formParams -> when (formParams) { is DefaultParams -> form.copy(params = formParams.copy(name = name) as P) is ExtendedParams -> form.copy(params = formParams.copy(name = name) as P) } }No estoy exactamente seguro de por qué es así, pero creo que tiene algo que ver con que P sea un genérico. Porque si reescribes el Mutador como este puedes omitir el otro y tampoco hay error.
class Mutator(val form: Form<Params>) { fun modifyName(name: String): Form<Params> = when (form.params) { is DefaultParams -> form.copy(params = form.params.copy(name = name)) // compile error is ExtendedParams -> form.copy(params = form.params.copy(name = name)) // compile error } }De hecho, creo que para su caso de uso no necesita genéricos y esta solución podría ser exactamente lo que necesita.
EDITAR:
si lo modificas a esto
class Mutator<out P : Params>(val form: Form<P>) { fun modifyName(name: String): Form<P> = when (form.params) { is DefaultParams -> form.copy(params = (form.params.copy(name = name) as P)) is ExtendedParams -> form.copy(params = (form.params.copy(name = name) as P)) else -> form } } debería funcionar como querías. Ya no tendrá un error de compilación. Sin embargo, dará una advertencia de que as P es un elenco sin control. Pero para nosotros es fácil ver que esto nunca será un problema. Supongo que el compilador no es lo suficientemente inteligente.
Debe convertir los parámetros a P porque eso es lo que espera el compilador para la función de copy . Sin embargo, esto le da una advertencia sobre una excepción de conversión no verificada (que es relativamente segura, consulte la respuesta de Sweeper ).
En caso de que desee evitar esta excepción, puede convertir la función modifyName en línea y usar parámetros reificados (consulte los documentos de Kotlin sobre conversiones no verificadas y esta excelente respuesta sobre la palabra clave reificada).
inline fun<reified P: Params> Form<P>.modifyName(name: String): Form<P> = when (this.params) { is DefaultParams -> copy(params = params.copy(name = name) as P) is ExtendedParams -> copy(params = params.copy(name = name) as P) else -> this } sealed interface Params data class DefaultParams(val name: String) : Params data class ExtendedParams(val name: String, val age: Int) : Params data class Form<out P : Params>(val params: P) fun main() { val form: Form<DefaultParams> = Form(DefaultParams("John")) val newForm: Form<DefaultParams> = form.modifyName("Joe") val nextForm: Form<ExtendedParams> = Form(ExtendedParams("John", 30)) val nextNewForm: Form<ExtendedParams> = nextForm.modifyName("Joe") } Esto alineará los tipos en el bytecode generado, de modo que cuando llame a form.modifyName("Joe") en el ejemplo anterior, la función modifyName sabrá que P es DefaultParams y, por lo tanto, puede devolver una instancia de Form<DefaultParams> (en lugar de Form<Params> ).