Genéricos em Kotlin
1. Visão geral
Neste artigo, veremosgeneric types in the Kotlin language.
Eles são muito semelhantes aos da linguagem Java, mas os criadores da linguagem Kotlin tentaram torná-los um pouco mais intuitivos e compreensíveis, introduzindo palavras-chave especiais comooutein.
2. Criação de classes parametrizadas
Digamos que queremos criar uma classe parametrizada. Podemos fazer isso facilmente na linguagem Kotlin usando tipos genéricos:
class ParameterizedClass(private val value: A) {
fun getValue(): A {
return value
}
}
Podemos criar uma instância dessa classe definindo um tipo parametrizado explicitamente ao usar o construtor:
val parameterizedClass = ParameterizedClass("string-value")
val res = parameterizedClass.getValue()
assertTrue(res is String)
Felizmente, o Kotlin pode inferir o tipo genérico do tipo de parâmetro, para que possamos omitir isso ao usar o construtor:
val parameterizedClass = ParameterizedClass("string-value")
val res = parameterizedClass.getValue()
assertTrue(res is String)
3. Kotlinout ein Palavras-chave
3.1. A palavra-chaveOut
Digamos que desejamos criar uma classe produtora que produzirá um resultado de algum tipo T. As vezes; queremos atribuir esse valor produzido a uma referência que é de um supertipo do tipo T.
Para conseguir isso usando Kotlin,we need to use theout keyword on the generic type. It means that we can assign this reference to any of its supertypes. The out value can be only be produced by the given class but not consumed:
class ParameterizedProducer(private val value: T) {
fun get(): T {
return value
}
}
Definimos uma classeParameterizedProducer que pode produzir um valor do tipo T.
Próximo; podemos atribuir uma instância da classeParameterizedProducer à referência que é um supertipo dela:
val parameterizedProducer = ParameterizedProducer("string")
val ref: ParameterizedProducer = parameterizedProducer
assertTrue(ref is ParameterizedProducer)
Se o tipoT na classeParamaterizedProducer não for o tipoout, a instrução fornecida produzirá um erro do compilador.
3.2. A palavra-chavein
Às vezes, temos uma situação oposta, significando que temos uma referência do tipoTe queremos poder atribuí-la ao subtipoT.
We can use the in keyword on the generic type if we want to assign it to the reference of its subtype. The in keyword can be used only on the parameter type that is consumed, not produced:
class ParameterizedConsumer {
fun toString(value: T): String {
return value.toString()
}
}
Declaramos que um métodotoString() consumirá apenas um valor do tipoT.
A seguir, podemos atribuir uma referência do tipoNumber à referência de seu subtipo -Double:
val parameterizedConsumer = ParameterizedConsumer()
val ref: ParameterizedConsumer = parameterizedConsumer
assertTrue(ref is ParameterizedConsumer)
Se o tipoT emParameterizedCounsumer não for o tipoin, a instrução fornecida produzirá um erro do compilador.
4. Projeções de tipo
4.1. Copiar uma matriz de subtipos para uma matriz de supertipos
Digamos que temos um array de algum tipo e queremos copiar todo o array para o array do tipoAny. É uma operação válida, mas para permitir que o compilador compile nosso código, precisamos anotar o parâmetro de entrada com a palavra-chaveout.
Isso permite que o compilador saiba que o argumento de entrada pode ser de qualquer tipo que seja um subtipo deAny:
fun copy(from: Array, to: Array) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
Se o parâmetrofrom não for do tipoout Any, não poderemos passar uma matriz de um tipoInt como argumento:
val ints: Array = arrayOf(1, 2, 3)
val any: Array = arrayOfNulls(3)
copy(ints, any)
assertEquals(any[0], 1)
assertEquals(any[1], 2)
assertEquals(any[2], 3)
4.2. Adicionando elementos de um subtipo a uma matriz de seu supertipo
Digamos que temos a seguinte situação - temos uma matriz do tipoAny que é um supertipo deInte queremos adicionar um elementoInt a esta matriz. Precisamos usar a palavra-chavein como um tipo de array de destino para que o compilador saiba que podemos copiar o valorInt para este array:
fun fill(dest: Array, value: Int) {
dest[0] = value
}
Então, podemos copiar um valor do tipoInt para o array deAny:
val objects: Array = arrayOfNulls(1)
fill(objects, 1)
assertEquals(objects[0], 1)
4.3. Projeções de estrelas
Há situações em que não nos importamos com o tipo específico do valor. Digamos que queremos apenas imprimir todos os elementos de uma matriz e não importa o tipo dos elementos nesta matriz.
Para conseguir isso, podemos usar uma projeção em estrela:
fun printArray(array: Array<*>) {
array.forEach { println(it) }
}
Então, podemos passar uma matriz de qualquer tipo para o métodoprintArray():
val array = arrayOf(1,2,3)
printArray(array)
Ao usar o tipo de referência de projeção em estrela, podemos ler valores dele, mas não podemos escrevê-los porque isso causará um erro de compilação.
5. Restrições Genéricas
Digamos que queremos classificar uma matriz de elementos, e cada tipo de elemento deve implementar uma interfaceComparable. Podemos usar as restrições genéricas para especificar esse requisito:
fun > sort(list: List): List {
return list.sorted()
}
No exemplo dado, definimos que todos os elementosT necessários para implementar a interfaceComparable. Caso contrário, se tentarmos passar uma lista de elementos que não implementam essa interface, isso causará um erro no compilador.
Definimos uma funçãosort que leva como argumento uma lista de elementos que implementamComparable, para que possamos chamar o métodosorted() nela. Vejamos o caso de teste desse método:
val listOfInts = listOf(5,2,3,4,1)
val sorted = sort(listOfInts)
assertEquals(sorted, listOf(1,2,3,4,5))
Podemos facilmente passar uma lista deInts porque o tipoInt implementa a interfaceComparable.
6. Genéricos em tempo de execução
6.1. Tipo Erasure
Assim como no Java, os genéricos do Kotlin sãoerased em tempo de execução. Ou seja,an instance of a generic class doesn’t preserve its type parameters at runtime.
Por exemplo, se criarmos umSet<String>e colocarmos algumas strings nele, em tempo de execução, só poderemos vê-lo comoSet.
Vamos criar doisSets com dois parâmetros de tipo diferentes:
val books: Set = setOf("1984", "Brave new world")
val primes: Set = setOf(2, 3, 11)
Em tempo de execução, as informações de tipo paraSet<String>eSet<Int> serão apagadas e vemos ambos comoSets. simples, embora seja perfeitamente possível descobrir em tempo de execução que o valor é umSet, não podemos dizer se é umSet de strings, inteiros ou outra coisa:that information has been erased.
Então, como o compilador de Kotlin nos impede de adicionar umNon-String em umSet<String>? Ou, quando obtemos um elemento de aSet<String>, como ele sabe que o elemento é aString?
A resposta é simples. The compiler is the one responsible for erasing the type information, mas antes disso, ele realmente sabe que a variávelbooks contémString elementos.
Portanto, toda vez que obtemos um elemento dele, o compilador o transforma emString ou quando adicionarmos um elemento a ele, o compilador digitaria verificar a entrada.
6.2. Parâmetros de tipo reificado
Vamos nos divertir mais com os genéricos e criar uma função de extensão para filtrar os elementosCollection com base em seu tipo:
fun Iterable<*>.filterIsInstance() = filter { it is T }
Error: Cannot check for instance of erased type: T
A parte “it is T”, para cada elemento da coleção, verifica se o elemento é uma instância do tipoT, mas como as informações de tipo foram apagadas em tempo de execução, não podemos refletir sobre os parâmetros de tipo desta forma .
Ou podemos?
A regra de eliminação de tipo é verdadeira em geral, mas há um caso em que podemos evitar essa limitação:Inline functions. Type parameters of inline functions can be reified, so we can refer to those type parameters at runtime.
O corpo das funções embutidas está embutido. Ou seja, o compilador substitui o corpo diretamente em locais onde a função é chamada, em vez da invocação normal da função.
Se declararmos a função anterior comoinlinee marcarmos o parâmetro de tipo comoreified, poderemos acessar as informações do tipo genérico em tempo de execução:
inline fun Iterable<*>.filterIsInstance() = filter { it is T }
A reificação em linha funciona como um encanto:
>> val set = setOf("1984", 2, 3, "Brave new world", 11)
>> println(set.filterIsInstance())
[2, 3, 11]
Vamos escrever outro exemplo. Todos nós estamos familiarizados com as definições típicas de SLF4jLogger:
class User {
private val log = LoggerFactory.getLogger(User::class.java)
// ...
}
Usando funções embutidas reificadas, podemos escrever definições deLogger mais elegantes e menos horripilantes de sintaxe:
inline fun logger(): Logger = LoggerFactory.getLogger(T::class.java)
Então podemos escrever:
class User {
private val log = logger()
// ...
}
Isso nos dá uma opção mais limpa para implementarlogging, the Kotlin way.
6.3. Mergulhe na Reificação Inline
Então, o que há de tão especial nas funções inline para que a reificação de tipo funcione apenas com elas? Como sabemos, o compilador do Kotlin copia o bytecode das funções inline para os locais onde a função é chamada.
Como em cada site de chamada, o compilador conhece o tipo de parâmetro exato, ele pode substituir o parâmetro de tipo genérico pelas referências de tipo reais.
Por exemplo, quando escrevemos:
class User {
private val log = logger()
// ...
}
When the compiler inlines the logger<User>() function call, it knows the actual generic type parameter –User. Portanto, em vez de apagar as informações de tipo, o compilador aproveita a oportunidade de reificação e reifica o parâmetro de tipo real.
7. Conclusão
Neste artigo, analisamos os tipos genéricos de Kotlin. Vimos como usar as palavras-chaveoutein corretamente. Usamos projeções de tipo e definimos um método genérico que usa restrições genéricas.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.