Introdução a Generics em Kotlin
Generics (tipos genéricos) são um dos recursos principais de linguagens de programação estaticamente tipadas, incluindo Kotlin. Eles permitem escrever código flexível e reutilizável que funciona com diferentes tipos de dados, mantendo a segurança de tipos estrita em tempo de compilação.
Imagine que você precisa criar uma estrutura de dados para armazenar uma lista de elementos. Sem generics, você teria que escrever classes separadas para cada tipo (IntList, StringList, UserList). Com generics, basta uma classe MyList<T> que funcionará com qualquer tipo.
// Sem Generics — é necessário duplicar códigoclass IntList(private val items: MutableList<Int>)class StringList(private val items: MutableList<String>)
// Com Generics — uma classe para todos os tiposclass GenericList<T>(private val items: MutableList<T>) { fun add(item: T) { items.add(item) } fun get(index: Int): T = items[index]}Neste artigo, vamos detalhar como generics funcionam em Kotlin, abordar sua sintaxe, casos de uso e conceitos avançados como covariância e contravariância.
Fundamentos do trabalho com Generics
Declaração de classes e funções genéricas
Para declarar uma classe genérica, basta especificar um ou mais parâmetros de tipo entre colchetes angulares após o nome da classe:
class Box<T>(var value: T) { fun getValue(): T = value fun setValue(newValue: T) { value = newValue }}
// Usoval intBox = Box<Int>(42)val stringBox = Box<String>("Hello")
// Kotlin consegue inferir o tipo, então podemos escrever de forma mais curtaval anotherBox = Box("Auto type inference")Funções também podem ser genéricas. O parâmetro de tipo é especificado antes do nome da função:
fun <T> printItem(item: T) { println("Item: $item")}
fun <T> createPair(first: T, second: T): Pair<T, T> { return Pair(first, second)}
// UsoprintItem(123)printItem("Text")val pair = createPair("A", "B")Restrições de tipo (Type Bounds)
Às vezes, é necessário restringir os tipos que podem ser usados em generics. Para isso, são usados limites superiores (upper bounds) com a palavra-chave : Type:
// Apenas tipos que implementam Comparablefun <T : Comparable<T>> findMax(a: T, b: T): T { return if (a > b) a else b}
// Múltiplas restrições com wherefun <T> processItem(item: T) where T : CharSequence, T : Comparable<T> { println("Length: ${item.length}") println("Compared: ${item.compareTo(item)}")}
// Usoprintln(findMax(10, 20)) // 20println(findMax("Apple", "Banana")) // "Banana"// findMax(10, "Text") // Erro de compilação!Variância: in, out e projeção de tipos
Um dos tópicos mais complexos, mas importantes em generics, é a variância (variance). Ela define como tipos genéricos se relacionam entre si durante a herança.
Invariância (padrão)
Por padrão, generics em Kotlin são invariantes. Isso significa que Box<Number> e Box<Int> não são subtipos um do outro, mesmo que Int seja um subtipo de Number:
val intBox: Box<Int> = Box(42)// val numBox: Box<Number> = intBox // Erro de compilação!Covariância (out)
O modificador out torna o tipo covariante. Isso significa que Box<Int> se torna um subtipo de Box<Number>. É usado quando o tipo T é usado apenas em posições de saída (valores de retorno):
class Producer<out T>(private val item: T) { fun produce(): T = item // T apenas na saída}