Skip to main content

Varianza en sitio de uso

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/

La varianza es un concepto esencial en la programación genérica que determina cómo los tipos genéricos se comportan en relación con sus subtipos y supertipos. En Kotlin, además de la varianza en sitio de declaración que ya conoces, existe la varianza en sitio de uso, que nos permite especificar la varianza de un tipo genérico en el punto donde lo utilizamos, sin modificar su declaración original.

Este enfoque es particularmente útil cuando trabajamos con bibliotecas de software que utilizan clases o interfaces genéricas invariables, y necesitamos flexibilidad adicional al utilizarlas en nuestros propios proyectos.

¿Por qué necesitamos varianza en sitio de uso?

Imagina que estás utilizando una biblioteca que define una clase genérica invariante:

class Box<T>(val value: T)

Si queremos escribir una función que pueda aceptar un Box de cualquier subtipo de Animal, no podemos hacerlo directamente debido a la invarianza. Aquí es donde la varianza en sitio de uso entra en juego.

Proyecciones de tipo

La varianza en sitio de uso se logra mediante proyecciones de tipo, que nos permiten adaptar la varianza de un tipo genérico en el punto de uso.

Sintaxis de proyecciones

  • Covarianza en sitio de uso: out T
  • Contravarianza en sitio de uso: in T

Estas palabras clave se utilizan al especificar el tipo genérico en una función o clase.

Ejemplo práctico: Procesamiento de eventos

Supongamos que estamos trabajando con una biblioteca que define una clase EventHandler invariante:

class EventHandler<T> {
fun handle(event: T) {
// Manejo del evento
}
}

Queremos escribir una función que pueda registrar un EventHandler para cualquier supertipo de ClickEvent.

interface Event
class ClickEvent : Event
class HoverEvent : Event
fun registerClickHandler(handler: EventHandler<ClickEvent>) {
// Registro del handler
}

El problema aquí es que no podemos pasar un EventHandler<Event> a esta función, debido a la invarianza.

Solución con varianza en sitio de uso

Podemos utilizar una proyección de tipo para permitir que la función acepte EventHandler de tipos más generales:

fun registerClickHandler(handler: EventHandler<in ClickEvent>) {
// Registro del handler
}

Ahora, podemos pasar un EventHandler<Event> a esta función:

val generalHandler = EventHandler<Event>()
registerClickHandler(generalHandler) // Funciona correctamente

Beneficios

  • Flexibilidad: La varianza en sitio de uso permite adaptar tipos invariables para que sean covariantes o contravariantes sin modificar su declaración original, lo que brinda mayor flexibilidad al trabajar con bibliotecas de software.
  • Seguridad de tipos: Aunque se utiliza en el punto de uso, las proyecciones de tipo (in y out) mantienen la seguridad de tipos en tiempo de compilación, reduciendo la posibilidad de errores en tiempo de ejecución.
  • Interoperabilidad: Facilita la integración con bibliotecas externas que no han declarado la varianza en sus clases o interfaces genéricas, permitiendo reutilizar código de manera más eficiente.
  • Simplicidad en código existente: No requiere cambiar la declaración original de una clase o interfaz genérica, lo que hace que su uso sea más directo y compatible con bibliotecas preexistentes.

Limitaciones

  • Acceso restringido: Las proyecciones de tipo pueden limitar el acceso a ciertos métodos o propiedades del tipo genérico. Por ejemplo, en una proyección de tipo in, solo puedes consumir el tipo, pero no producirlo.
  • Dificultad en la lectura: Aunque proporciona flexibilidad, el uso extensivo de in y out en el código puede hacer que sea más difícil de leer y mantener, ya que se necesita comprender cómo afecta la varianza en cada caso de uso.

Proyecciones de estrella (*)

En ocasiones, no nos importa el tipo genérico específico, y solo queremos acceder a los miembros que no dependen de él. Para estos casos, Kotlin ofrece las proyecciones de estrella.

Ejemplo con proyección de estrella

Supongamos que queremos imprimir el tamaño de una lista, sin importar su tipo de elemento:

fun printListSize(list: List<*>) =
println("La lista tiene ${list.size} elementos")

Aquí, List<*> es una lista de algún tipo desconocido, y solo podemos acceder a los miembros que no dependen del tipo genérico T.

Beneficios

  • Simplicidad: Las proyecciones de estrella (*) simplifican el manejo de tipos genéricos cuando no importa el tipo específico. Esto es útil para escribir funciones que solo necesitan trabajar con las propiedades comunes de una estructura, como el tamaño de una lista.
  • Versatilidad: Permite trabajar con cualquier instancia de un tipo genérico sin conocer o especificar su tipo exacto, lo que facilita la integración con bibliotecas y código de terceros donde los tipos genéricos pueden ser variados.
  • Compatibilidad en funciones utilitarias: Es ideal para escribir funciones utilitarias genéricas que solo necesitan leer datos y no modificar la colección o estructura, garantizando seguridad y flexibilidad.

Limitaciones

  • Acceso limitado: El uso de proyecciones de estrella restringe el acceso a elementos específicos del tipo genérico. Solo se puede interactuar con miembros que no dependan del tipo genérico, limitando la funcionalidad en ciertos contextos.
  • Pérdida de información de tipo: Al utilizar *, se pierde la información del tipo específico, lo que puede hacer que algunas operaciones que dependen de esta información no sean posibles, afectando la expresividad y precisión del código.
  • Menor seguridad de tipos: Comparado con el uso de tipos específicos o proyecciones de varianza (in y out), el uso de * sacrifica la seguridad de tipos que el compilador podría proporcionar, aumentando el riesgo de errores sutiles.
Ejercicio

Imagina que estás usando una biblioteca que define una interfaz Serializer invariante:

interface Serializer<T> {
fun serialize(value: T): String
}

Escribe una función genérica serializeValue que pueda aceptar un Serializer de modo que el siguiente código funcione correctamente:

val anySerializer = object : Serializer<Any> {
override fun serialize(value: Any) =
value.toString()
}

serializeValue(anySerializer, 42) // Debería imprimir "42"
serializeValue(anySerializer, "Hello") // Debería imprimir "Hello"
Solución
fun <T> serializeValue(serializer: Serializer<in T>, value: T) =
serializer.serialize(value)

¿Qué aprendimos?

En esta lección, exploramos la varianza en sitio de uso en Kotlin, un mecanismo poderoso que proporciona flexibilidad adicional al trabajar con tipos genéricos invariantes. La varianza en sitio de uso nos permite adaptar tipos en el punto de uso utilizando las proyecciones in y out, sin necesidad de modificar la declaración original de clases o interfaces genéricas.

Puntos clave:

  1. Flexibilidad en el uso de tipos genéricos: La varianza en sitio de uso permite utilizar tipos genéricos de forma más flexible, adaptándolos para que sean covariantes o contravariantes según sea necesario.
  2. Proyecciones de estrella (*): Estas proyecciones son útiles cuando no importa el tipo específico, facilitando la escritura de funciones genéricas que interactúan con colecciones o estructuras de manera segura y sencilla.
  3. Compatibilidad con bibliotecas externas: La varianza en sitio de uso facilita la integración de bibliotecas que no tienen varianza declarada, permitiendo aprovechar sus tipos genéricos sin restricciones adicionales.
  4. Limitaciones y consideraciones: Aunque la varianza en sitio de uso proporciona flexibilidad, también puede restringir el acceso a ciertos métodos y hacer que el código sea más difícil de leer y mantener si se abusa de ella.

En resumen, la varianza en sitio de uso es una herramienta esencial en Kotlin que, cuando se usa correctamente, puede mejorar significativamente la reutilización de código y la interoperabilidad con bibliotecas de terceros.

Bibliografías Recomendadas

  • 📚 "9. Generics". (2017). Dmitry Jemerov & Svetlana Isakova, en Kotlin in Action, (pp. 223–253.) Manning Publications Co.