Programação Avançada Kotlin Parte 4

Tempo de leitura: 5 minutes

Nesta postagem, vamos explorar alguns conceitos avançados de funções inline e tipo reificado no Kotlin.

 

O que são funções inline?

Em Kotlin inline é uma palavra-chave. As funções embutidas são as funções normais, mas anexadas a uma palavra-chave embutida. As funções inline têm o objetivo de copiar o conteúdo da função para o site de chamada junto com outros parâmetros para reduzir as alocações de memória. Cada função de ordem superior em nosso código levará à criação de um objeto de função que precisa de uma alocação de memória, portanto, introduz uma sobrecarga de tempo de execução. Podemos evitar a criação desse objeto de função adicionando a palavra-chave embutida às respectivas funções.

Observação: se estivermos tentando marcar alguma função como embutida, que não aceita outra função como parâmetro, não haverá benefícios de desempenho significativos.

 

Função inline – sob o capô

O que acontece nos bastidores é que com a palavra-chave embutida, o compilador copia o conteúdo da função embutida para o site de chamada, evitando alocações de memória desnecessárias para novos objetos de função.

fun sampleFunction() {
     print("Bem-vindo msg1")
     doSomething()
     print("Bem-vindo msg3")
 }

 fun doSomething() {
     print("Bem-vindo msg2")
 }

Agora vamos verificar como o código de bytes Kotlin é gerado. Para isso, no Android Studio, vá para Ferramentas -> Kotlin -> Mostrar Bytecode Kotlin = (Tools -> Kotlin -> Show Kotlin Bytecode), uma janela será mostrada no lado direito

É a geração normal de código conforme escrito, mas se você adicionar inline ao método doSomething, ele mudaria da seguinte maneira

Para entender, seria algo como abaixo

fun sampleFunction() {
     print("Bem-vindo msg1")
     print("Bem-vindo msg2")
     print("Bem-vindo msg3")
}

Nota: Como o inlining copia as funções para vários sites de chamada, pode aumentar muito o código gerado. Portanto, certifique-se de evitar funções grandes embutidas.

 

Por que precisamos de funções embutidas?

Para reduzir a sobrecarga de memória de funções de ordem superior ou expressão lambda, usamos funções inline. Adicione uma palavra-chave embutida às funções que recebem outras funções como um parâmetro ou têm parâmetros do tipo reificado para obter melhores benefícios de desempenho. Alocações de memória para objetos de função e classes, chamadas virtuais introduzem sobrecarga de tempo de execução sem inline.

Ao usar funções inline, não temos permissão para manter uma referência às funções passadas como um parâmetro ou passá-la para uma função diferente, caso contrário, obteremos um erro do compilador dizendo Uso ilegal de parâmetro inline. Nesses casos, precisamos usar o modificador noinline para os parâmetros.

Não use inline para um método com um único parâmetro que também está marcado com noinline, pois oferece benefícios de baixo desempenho.

 

O que é modificador noinline?

Se uma função aceita mais de uma função como parâmetro de tipo, e se não queremos embutir todos os parâmetros, podemos especificar a palavra-chave noinline na frente dos parâmetros que não queremos embutir. Lambdas embutidos só podem ser chamados dentro das funções embutidas ou passados ​​como argumentos embutidos, mas os noinline podem ser manipulados da maneira que quisermos: armazenados em campos, passados, etc.

inline fun doSomething(first: () -> String, noinline second: () -> String) {  
 ....
}

Nota: Se uma função embutida não tem parâmetros de função embutidos e nenhum parâmetro de tipo reificado, o compilador irá emitir um aviso dizendo que tais funções são muito improváveis ​​de serem benéficas

 

O que são genéricos?

Em Java e na maioria das outras linguagens, existe um conceito chamado genéricos. Genérico é um conceito ou estilo de programação usado para escrever algum código genérico independente do tipo. Os genéricos estendem o sistema de tipos para permitir que um tipo ou método opere em um objeto de diferentes tipos, enquanto fornece segurança de tipo em tempo de compilação. Por exemplo, existe um método que pode receber Inteiro e String como entrada e executar a lógica comum de impressão dos valores

fun sample(){
        doSomething(1)
        doSomething("Oi")
  }
    
 fun <T> doSomething(element: T) {
        println(element)
 }

O código acima é executado normalmente e dá o resultado esperado. Se tentarmos acessar o tipo de <T> em tempo de execução usando T :: class.java, o IDE mostra um erro: não pode usar T como um parâmetro de tipo reificado.

 

Type Erasure é um conceito em genéricos Java que usa o compilador Java para traduzir o tipo genérico em tipo bruto. É um processo de impor restrições de tipo apenas em tempo de compilação e descartar as informações de tipo de elemento em tempo de execução. Isso nos fornece a segurança de tipo e ajuda a evitar a necessidade de fundição explícita. No entanto, podemos usar reflexos ou API não java ou simplesmente passar o tipo de classe para o método onde é necessário, conforme a seguir

fun sample(){
        doSomething(Int::class.java,1)
        doSomething(String::class.java,"Oi")
}

private fun <T> doSomething(classType: Class<T>,element: T) {
        println(element)
        println(classType)
}

Mas é um pouco complicado passar esse tipo para onde for necessário. Então aí vem o tipo reificado em Kotlin para o resgate.

 

O que é tipo reificado?

refied é uma palavra-chave com a qual podemos acessar a informação do tipo dentro de uma função genérica

tipo refied só pode ser usado com funções embutidas, então precisamos

  • Marque nossa função como inline e
  • Adicione a palavra-chave refied ao parâmetro genérico.

As mesmas regras que se aplicam às funções inline serão aplicáveis ​​ao refied. Então, finalmente, podemos acessar as informações de tipo de <T> usando reified inline, conforme mostrado abaixo:

fun sample(){
       doSomething(1)
       doSomething("Oi")
   }
   private inline fun <reified T> doSomething(element: T) {
       println(element)
       println(T::class.java)
   }

Isso acima é possível porque as funções embutidas copiam o corpo da função para chamar site e substituem o tipo genérico pelo tipo necessário para que esse tipo seja conhecido em tempo de execução.

 

refied para funções de sobrecarga

reified também permite funções de sobrecarga para retornar tipos genéricos. Normalmente, uma função não pode ser sobrecarregada com a mesma entrada e ter diferentes tipos de retorno. Mas com funções embutidas, o compilador pode substituir o tipo de retorno genérico pelo tipo esperado, copiando o corpo da função.

inline fun<reified T> doSomething(marks: Int): T {
    return when (T::class) {
        Int::class -> marks as T
        Float::class -> marks.toFloat() as T
        else -> "Apenas FLoat e Int são suportados" 
    }
}

val a:Int = doSomething(123)
val b:Float = doSomething(123)

reified nos fornece a flexibilidade de fazer coisas com genéricos que eram anteriormente restritos devido à falta de informações de tipo em tempo de execução. reified não afeta o desempenho.

“funções reified não podem ser acessadas de Java. Como inlining não é compatível com Java, os parâmetros genéricos não podem deixar de ser apagados pelo compilador”

 

Resumo

Para diminuir o custo de alocações de memória causadas por expressões lambda, use a palavra-chave inline. Use em linha com pequenas funções. Marque o parâmetro da função como não embutido se quiser manter ou passar a referência dele. A palavra-chave reificada é usada para acessar as informações de tipo do objeto em funções genéricas.