Varianza en sitio de declaración
⏱ 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 fundamental en lenguajes de programación genéricos que describe cómo los tipos genéricos se comportan en relación con sus subtipos. En Kotlin, la varianza se puede especificar en el sitio de declaración, lo que significa que definimos cómo se comportará un parámetro de tipo genérico en el momento en que declaramos la clase o interfaz genérica.
Este enfoque contrasta con la varianza en sitio de uso, donde especificamos la varianza al utilizar el tipo genérico. En esta lección, exploraremos la varianza en sitio de declaración en Kotlin y cómo aplicarla en el contexto de bibliotecas de software, para crear APIs más seguras y flexibles.
Varianza
- Covarianza (
out
): Permite que un tipo genérico acepte subtipos del parámetro de tipo especificado. Se utiliza cuando el tipo genérico produce valores de tipoT
. - Contravarianza (
in
): Permite que un tipo genérico acepte supertipos del parámetro de tipo especificado. Se utiliza cuando el tipo genérico consume valores de tipoT
. - Invarianza: El tipo genérico solo acepta exactamente el tipo especificado; ni subtipos ni supertipos.
Varianza en Sitio de Declaración
En Kotlin, podemos declarar la varianza de un parámetro de tipo directamente en la definición de una clase o interfaz genérica, usando las palabras clave in
y out
. Esto se conoce como varianza en sitio de declaración.
interface Producer<out T> {
fun produce(): T
}
interface Consumer<in T> {
fun consume(item: T)
}
Producer<out T>
: DeclaraT
como covariante. Esto significa que siA
es una subclase deB
, entoncesProducer<A>
es un subtipo deProducer<B>
.Consumer<in T>
: DeclaraT
como contravariante. Esto significa que siA
es una subclase deB
, entoncesConsumer<B>
es un subtipo deConsumer<A>
.
Ejemplo Práctico: Biblioteca de Procesamiento de Datos
Supongamos que estamos desarrollando una biblioteca que maneja procesadores de datos. Tenemos una jerarquía de tipos:
class Data
class TextData : Data()
class ImageData : Data()
Queremos definir una interfaz DataProcessor
que procese datos:
interface DataProcessor<in T> {
fun process(data: T)
}
Aquí, usamos in T
para indicar que T
es contravariante, ya que DataProcessor
consume datos de tipo T
.
Gracias a la contravarianza, podemos hacer lo siguiente:
class GeneralProcessor : DataProcessor<Data> {
override fun process(data: Data) =
println("Processing data")
}
val generalProcessor: DataProcessor<Data> = GeneralProcessor()
val textProcessor: DataProcessor<TextData> = generalProcessor
DataProcessor<in T>
es contravariante enT
.- Dado que
TextData
es un subtipo deData
, y debido a la contravarianza, podemos asignar unDataProcessor<Data>
a una variable de tipoDataProcessor<TextData>
.
Esto nos permite reutilizar procesadores generales para tipos de datos más específicos.
Ejemplo: Colecciones Inmutables
Las colecciones inmutables en Kotlin están diseñadas utilizando varianza en sitio de declaración.
interface List<out E> : Collection<E> {
operator fun get(index: Int): E
// ...
}
Aquí, E
está declarado como out E
, lo que significa que List
es covariante en E
.
Podemos asignar una List<TextData>
a una variable de tipo List<Data>
:
val textDataList: List<TextData> = listOf(TextData(), TextData())
val dataList: List<Data> = textDataList // Válido gracias a la covarianza
Beneficios
- Flexibilidad de APIs: La varianza en sitio de declaración permite diseñar APIs más flexibles que pueden trabajar con jerarquías de tipos más amplias. Esto facilita que las bibliotecas sean más adaptables y reutilizables en diferentes contextos, mejorando la experiencia de lxs desarrolladorxs que las utilizan.
- Seguridad de tipos en tiempo de compilación: Al especificar la varianza en la declaración, el compilador puede garantizar la seguridad de tipos, evitando errores en tiempo de ejecución relacionados con incompatibilidades de tipos. Esto resulta en código más robusto y confiable.
- Código más claro y mantenible: Declarar la varianza directamente en la definición de clases o interfaces hace que las intenciones del código sean más explícitas. Esto mejora la legibilidad y facilita el mantenimiento, ya que otrxs desarrolladorxs pueden entender rápidamente cómo se espera que se utilicen los tipos genéricos.
- Reutilización de implementaciones genéricas: Permite reutilizar implementaciones generales en contextos más específicos sin necesidad de duplicar código. Por ejemplo, un
DataProcessor<Data>
puede ser utilizado comoDataProcessor<TextData>
gracias a la contravarianza.
Limitaciones
- Complejidad conceptual: Entender y aplicar correctamente la varianza en sitio de declaración puede ser complicado, especialmente para desarrolladorxs menos experimentadxs. Esto puede llevar a confusiones y errores sutiles si no se comprende completamente cómo funciona.
- Restricciones en el uso de tipos genéricos: Las restricciones impuestas por la varianza pueden limitar cómo se utilizan los parámetros de tipo dentro de la clase o interfaz. Por ejemplo, en una clase covariante (
out T
), no puedes usarT
en posiciones de entrada, lo que puede ser limitante en ciertos casos. - Mensajes de error difíciles de interpretar: Los errores del compilador relacionados con varianza pueden ser difíciles de entender y diagnosticar, lo que puede aumentar el tiempo de desarrollo y depuración.
Ejercicio
Implementa una interfaz Logger
para una biblioteca de registro de eventos.
- Declara un método
log: () -> M
que devuelve un mensaje de tipoM
. - ¿Qué tipo de varianza debería tener
Logger
enM
y por qué?
Solución
-
interface Logger<out M> {
fun log(): M
} Logger
debe ser covariante enM
. Esto permite que unLogger<String>
sea asignado a unLogger<Any>
, ya que unLogger<String>
puede producir mensajes de tipoString
, que es un subtipo deAny
.
Bibliografías Recomendadas
- 📚 "Generics". (2017). Dmitry Jemerov & Svetlana Isakova, en Kotlin in Action, (pp. 223–253.) Manning Publications Co.