Dominar as funções padrão do Kotlin: run, with, let, also e apply

Tempo de leitura: 5 minutes

Algumas das funções padrão do Kotlin são tão semelhantes que não temos certeza de qual usar. Aqui, apresentarei uma maneira simples de distinguir claramente suas diferenças e como escolher qual usar.

Funções de escopo

As funções nas quais irei me concentrar são run, with, T.run, T.let, T.also e T.apply. Eu as chamo de funções de escopo, pois vejo que sua função principal é fornecer um escopo interno para a função do chamador.

A maneira mais simples de ilustrar o escopo é a função de execução

fun test() {
    var mood = "Estou triste"  
    run {
        val mood = "Estou feliz"
        println(mood) // Estou feliz
    }
    println(mood)  // estou triste
}

Com isso, dentro da função de “test”, você poderia ter um escopo separado, onde o “mood” é redefinido para “Estou feliz” antes de imprimir, e é totalmente incluído no escopo de execução.

Esta função de definição de escopo em si não parece muito útil. Mas há outra parte legal que tem mais do que apenas o escopo; ele retorna algo, ou seja, o último objeto dentro do escopo.

Conseqüentemente, o abaixo seria legal, por meio do qual podemos aplicar o show() a ambas as visualizações, sem chamá-lo duas vezes.

run {
        if (firstTimeView) introView else normalView
    }.show()

3 atributos de funções de escpo

Para tornar as funções de escopo mais interessantes, deixe-me categorizar seu comportamento com 3 atributos. Usarei esses atributos para distingui-los uns dos outros.

1. Função normal vs. extensão

Se olharmos para ‘with’ e T.run, ambas as funções são bastante semelhantes. O seguinte faz a mesma coisa.

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}

// Similar

webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

No entanto, sua diferença é que uma é uma função normal, ou seja, “with”, enquanto a outra é uma função de extensão, ou seja, T.run.

Portanto, a questão é: qual é a vantagem de cada um?

Imagine se webview.settings pudesse ser nulo, eles teriam a aparência abaixo.

// Uau!
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}

// Agradável.

webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

Nesse caso, a função de extensão T.run é melhor, pois poderíamos aplicar para verificar se há nulidade antes de usá-la.

2. Este argumento contra isso

Se olharmos para T.run e T.let, ambas as funções são semelhantes, exceto por uma coisa, a maneira como aceitam o argumento. O seguinte mostra a mesma lógica para ambas as funções.

stringVariable?.run {
      println("O comprimento desta string é $length")
}

// Similarly.

stringVariable?.let {
      println("O comprimento desta string é ${it.length}")
}

Se você verificar a assinatura da função T.run, notará que T.run é feito apenas como um bloco de chamada de função de extensão: T. (). Conseqüentemente, tudo dentro do escopo, o T poderia ser referido como this. Na programação, isso poderia ser omitido na maioria das vezes. Portanto, em nosso exemplo acima, poderíamos usar $length na instrução println, em vez de $ {this.length}. Eu chamo isso de enviar isso como um argumento.

No entanto, para a assinatura da função T.let, você notará que T.let está se enviando para a função, ou seja, “block: (T)”. Portanto, isso é como um argumento lambda enviado. Ele pode ser referido na função de escopo como ‘it’. Portanto, chamo isso de enviar como um argumento.

Do exposto acima, parece que T.run é mais superior sobre T.let, pois é mais implícito, mas existem algumas vantagens sutis da função T.let conforme abaixo: –

  • O T.let fornece uma distinção mais clara entre o uso da função/membro da variável fornecida e a função / membro da classe externa
  • No caso de “this” não poder ser omitido, por exemplo quando “it” é enviado como parâmetro de uma função, é mais curto de escrever do que “this” e mais claro.
  • O T.let permite nomear melhor a variável usada convertida, ou seja, você pode converter “it” para outro nome.
stringVariable?.let {
      nonNullString ->
      println("A string não nula é $nonNullString")
}

3. Retorne “this” contra outros ‘types”

Agora, vamos olhar para T.let e T. também, ambos são idênticos, se olharmos para o escopo de função interna dele.

stringVariable?.let {
      println("O comprimento desta string é ${it.length}")
}

// Exatamente o mesmo que abaixo

stringVariable?.also {
      println("O comprimento desta string é ${it.length}")
}

No entanto, sua diferença sutil é o que eles retornam. O T.let retorna um tipo diferente de valor, enquanto “T.also” retorna o próprio T, ou seja, “this”.

Ambos são úteis para funções de encadeamento, em que “T.let” permite que você evolua a operação e “T.also” permite que você execute a mesma variável, ou seja, this.

Ilustração simples como abaixo

val original = "abc"

// Evolua o valor e envie para a próxima cadeia
original.let {
    println("A string original é $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("A string reversa é $it") // "cba"
    it.length  // pode evoluir para outro tipo
}.let {
    println("O comprimento da corda é $it") // 3
}

// errado
// O mesmo valor é enviado na cadeia (a resposta impressa está errada)
original.also {
    println("A string original é$it") // "abc"
    it.reversed() // mesmo que o evoluamos, é inútil
}.also {
    println("A string reversa é ${it}") // "abc"
    it.length  // mesmo que o evoluamos, é inútil
}.also {
    println("O comprimento da corda é ${it}") // "abc"
}

// Corrigido para também (ou seja, manipular como string original
// O mesmo valor é enviado na cadeia
original.also {
    println("A string original é $it") // "abc"
}.also {
    println("A string reversa é ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

O T.also pode parecer sem sentido acima, já que poderíamos facilmente combiná-los em um único bloco de função. Pensando bem, tem algumas boas vantagens

  • Pode fornecer um processo de separação muito claro nos mesmos objetos, ou seja, fazer seções funcionais menores.
  • Pode ser muito poderoso para auto-manipulação antes de ser usado, fazendo uma operação de construtor de encadeamento.

Quando ambos combinam a cadeia, ou seja, um se desenvolve, o outro se mantém, torna-se algo poderoso, e. abaixo

// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}

// Abordagem aprimorada
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

Olhando para todos os atributos

Observando os 3 atributos, podemos saber muito bem o comportamento da função. Deixe-me ilustrar a função T.apply, já que ela não foi mencionada acima. Os 3 atributos de T.apply são os seguintes …

  • É uma função de extensão.
  • Envia this como seu argumento.
  • Ele retorna this (ou seja, ele mesmo)

Portanto, usando-o, pode-se imaginar, ele poderia ser usado como

// Normal approach
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}

// Improved approach
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

Ou também podemos tornar a criação de objetos desencadeados capaz de ser encadeada.

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

Seleção de funções

Portanto, claramente, com os 3 atributos, podemos agora categorizar as funções de acordo. E com base nisso, poderíamos formar uma árvore de decisão abaixo que poderia ajudar a decidir qual função queremos usar dependendo do que precisamos.

Esperamos que a árvore de decisão acima esclareça as funções e também simplifique sua tomada de decisão, permitindo que você domine o uso dessas funções de maneira adequada.

Sinta-se à vontade para fornecer um bom exemplo real de como você usa essas funções em resposta a este blog. Eu adoraria ouvir de você. Isso pode beneficiar outras pessoas.