Pular para o conteúdo

Trilha de Kotlin · parte 15 de 59

Funções de escopo e funções de extensão

Aula da fase 3 da trilha de Kotlin: Qual função de escopo usar em cada caso, e por que uma função de extensão não substitui um método virtual?

intermediárioPublicada em · 14 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Funções de escopo e funções de extensão

Imagine que você vai montar uma mesa de jantar. Você pode pegar cada talher, colocar no lugar e voltar para pegar o próximo, ou pode sentar à mesa e arrumar tudo de uma vez, sem ficar carregando as peças para lá e para cá. As funções de escopo do Kotlin são formas de dizer “trabalhe dentro deste objeto por um momento”. A diferença entre elas está em duas perguntas: como você se refere ao objeto dentro do bloco, e o que o bloco devolve no final.

As funções de extensão resolvem outro problema. Um String do Java não tem um método comMaiuscula(), e você não pode editar a classe String para acrescentá-lo. A extensão permite declarar esse método fora da classe, e o código que a usa escreve "ana".comMaiuscula() como se fosse um membro. Só que, por trás, o método não é virtual, e isso muda o resultado em casos que a maioria das pessoas não espera.

A trilha pede duas respostas. A primeira é qual função de escopo usar em cada caso. A segunda é por que uma função de extensão não substitui um método virtual. Os exemplos desta aula foram compilados e executados com o kotlinc 2.1.21.

1. o problema que as funções de escopo resolvem

Antes das funções de escopo, o código que configura um objeto costuma repetir o nome da variável várias vezes. Em Java, criar uma requisição com vários campos exige uma variável temporária e uma chamada para cada setter, e o objeto fica exposto a meio caminho de ser montado. O mesmo acontece com valores nuláveis, em que você precisa de uma variável só para checar se o valor existe antes de transformá-lo.

Essas repetições são pequenas, mas se acumulam. Elas criam variáveis que existem só para carregar um valor de uma linha para outra, e obrigam o leitor a acompanhar cada uma. As funções de escopo reduzem esse ruído porque o bloco fica sem nome para o objeto, ou com um nome curto e previsível.

Elas não são mágica, e a documentação oficial diz que as cinco são em grande parte intercambiáveis. A escolha depende da intenção do código, que é o que a aula vai treinar.

2. as cinco funções e o que as diferencia

Existem cinco funções de escopo no Kotlin: let, run, with, apply e also. Cada uma faz duas escolhas. A primeira é como o objeto aparece dentro do bloco: como this, que é o receptor implícito, ou como it, que é um parâmetro nomeado pelo padrão. A segunda é o que o bloco devolve: o próprio objeto, ou o resultado da última expressão do bloco.

A tabela da documentação oficial resume isso:

FunçãoReferência ao objetoValor devolvidoUso típico
letitresultado do blocotransformar valor, especialmente nulável
runthisresultado do blococonfigurar e calcular um resultado
withthis (passado como argumento)resultado do blocochamar várias funções de um objeto que já existe
applythiso próprio objetoconfigurar um objeto recém-criado
alsoito próprio objetoefeito colateral, como log, no meio de uma cadeia

Um detalhe importante é que run existe em duas formas. Como extensão, objeto.run { ... } usa this. Como função sem receptor, run { ... } apenas agrupa instruções onde uma expressão é exigida, e não tem objeto de contexto.

A diferença entre this e it tem consequências práticas. Com this, você escreve porta = 8080 e o compilador entende que a propriedade é do objeto. Com it, você escreve it.trim() e deixa o nome explícito, o que costuma ser mais claro em cadeias de transformações. Já a diferença entre o objeto e o resultado decide se o bloco é uma configuração ou um cálculo.

3. exemplos de cada uma

Para configurar um objeto recém-criado, apply é a escolha natural, porque devolve o próprio objeto e permite atribuir campos com nomes curtos:

data class Config(var host: String = "", var porta: Int = 0, var tls: Boolean = false)

fun montar(): Config = Config().apply {
    host = "api.exemplo.com"
    porta = 443
    tls = true
}

A saída foi Config(host=api.exemplo.com, porta=443, tls=true). O objeto que sai do apply já está pronto, e a função montar devolve esse valor sem variável intermediária.

Para transformar um valor que pode ser nulo, let combina bem com o operador ?.. O bloco só roda quando o valor existe, e o resultado já sai como nulável:

val email: String? = " ana@exemplo.com "
val tamanho = email?.let { it.trim().length }
println("let: $tamanho")

A saída foi let: 15. Se email fosse null, o let não rodaria e tamanho seria null, sem nenhum if escrito pelo programador.

Para calcular um resultado a partir de um objeto, run com this deixa a atribuição e a leitura no mesmo bloco:

val porta = Config().run {
    porta = 8080
    porta
}
println("run devolve: $porta")

A saída foi run devolve: 8080. O último valor do bloco é a porta, então esse é o resultado. Note que porta = 8080 atribui a propriedade de Config, enquanto a última linha porta lê a mesma propriedade, e o nome da variável externa val porta ainda não existe dentro do bloco. Esse tipo de sombreamento é uma das razões para não usar this com nomes iguais.

Para chamar várias funções de um objeto que já existe, sem precisar do resultado do bloco, with é a escolha clara:

val lista = mutableListOf("a", "b")
with(lista) {
    add("c")
    println("with: $size itens")
}

A saída foi with: 3 itens. Repare que with recebe o objeto como argumento, e não como receptor de uma extensão. Isso o torna útil quando o objeto não é o centro da operação, mas é o assunto de algumas linhas.

Por fim, also serve para olhar sem alterar a cadeia, como um log no meio de uma transformação:

val nomes = mutableListOf("ana").also { println("also antes: $it") }
println("also depois: ${nomes.size}")

A saída foi also antes: [ana] e depois also depois: 1. O also devolveu a própria lista, então nomes continua sendo a lista, e o println no meio serviu apenas como efeito colateral.

4. como escolher

Pergunte primeiro se o resultado final do bloco deve ser o objeto ou um valor calculado. Se for o objeto, a escolha fica entre apply e also, e a diferença está em como você se refere a ele: apply usa this, bom para configuração com nomes curtos, e also usa it, bom para efeitos que precisam deixar o nome explícito. Se for um valor calculado, a escolha fica entre let, run e with, e a diferença também está na referência: let usa it, bom para transformar valores nuláveis e encadear chamadas, e run ou with usam this, bons para trabalhar com as propriedades do objeto.

Qual função de escopo usar. O resultado é o objeto? → Usa o objeto como this? (sim); O resultado é o objeto? → Usa como it? (não)…
figura 1 · Qual função de escopo usar

O diagrama é uma árvore de decisão com duas perguntas. A primeira separa o caso em que o resultado é o objeto do caso em que o resultado é calculado. A segunda define se o objeto é usado como this ou como it. As folhas verdes são as que devolvem um valor, e as laranjas são as que devolvem o próprio objeto. A única função que o diagrama não mostra é with, que cai no mesmo ramo de run e serve quando o objeto é um argumento, e não o receptor.

Um sinal de que a escolha ficou ruim é a cadeia de funções de escopo aninhadas. Quando um let aparece dentro de um apply que está dentro de um run, cada nível muda o significado de this e de it, e o leitor precisa desenhar a pilha na cabeça. A documentação oficial recomenda evitar esse aninhamento, e a regra prática é simples: se o bloco não cabe em poucas linhas e não deixa claro de quem é cada referência, extraia uma função com nome.

5. funções de extensão e o que elas não são

Uma função de extensão declara um método que parece pertencer a uma classe, sem alterar a classe. A sintaxe é o nome do tipo receptor seguido de um ponto e da função. Dentro do corpo, this é o objeto receptor e pode ser omitido nas chamadas de membro:

fun String.comMaiuscula(): String =
    if (isEmpty()) this else this[0].uppercaseChar() + substring(1)

println("comMaiuscula: ${"ana".comMaiuscula()}")

A saída foi comMaiuscula: Ana. Por trás, o compilador gera um método estático que recebe a string como primeiro parâmetro. Essa é a razão pela qual a extensão não precisa de herança e pode ser aplicada até em tipos que você não controla, como String ou as coleções do Java.

Mas a extensão não é um método de verdade no sentido de polimorfismo. Ela é escolhida pelo tipo declarado da variável, e não pelo tipo real do objeto. O teste mostra a diferença com duas extensões de nomes parecidos:

open class Animal
class Gato : Animal()

fun Animal.descricao() = "extensão de Animal"
fun Gato.descricao() = "extensão de Gato"

val animal: Animal = Gato()
println("tipo declarado Animal: ${animal.descricao()}")
println("tipo declarado Gato: ${Gato().descricao()}")

A saída foi tipo declarado Animal: extensão de Animal. A variável guarda um Gato, mas a escolha foi feita pelo tipo Animal declarado. Quando o receptor é um Gato declarado como Gato, a extensão de Gato é chamada.

Isso é o oposto de um método virtual, e a diferença está no mecanismo. Um método virtual, como o area() de uma classe que outra classe sobrescreve, é resolvido em tempo de execução pelo tipo real do objeto. A JVM consulta a tabela de métodos do objeto e encontra a versão da subclasse. A extensão não entra nessa tabela, então a versão escolhida depende só do tipo que o compilador enxerga no ponto da chamada.

Membro virtual x extensão. forma.area() → Tipo real? (consulta a JVM); Tipo real? → Quadrado.area() (runtime: Quadrado)…
figura 2 · Membro virtual x extensão

O diagrama põe os dois mecanismos lado a lado. No grupo de cima, a chamada forma.area() passa pela consulta ao tipo real e chega ao override, que devolve 9.0. No grupo de baixo, animal.descricao() decide pelo tipo declarado e cai na versão de Animal, mesmo com um Gato dentro. A seta amarela marca o ponto em que a decisão deixa de depender do objeto e passa a depender da declaração.

Essa diferença tem consequências concretas. Se você escreve uma extensão para comportamento que deveria variar entre subtipos, ela não vai variar: a versão escolhida depende do tipo declarado e não do objeto. Por isso a regra de ouro é usar extensão para comportamento que não depende de subtipos, como formatação ou conversão de um tipo que você não controla, e usar membro virtual quando o comportamento deve mudar por subclasse.

Há mais uma limitação importante. Uma extensão não tem acesso aos membros privados da classe que estende. O código a seguir não compila:

class Conta(private val saldo: Int)
fun Conta.revelar() = saldo

A mensagem foi cannot access 'val saldo: Int': it is private in '/Conta'. Essa regra preserva o encapsulamento, porque a extensão fica do lado de fora da classe. Ela também significa que uma extensão não pode quebrar invariantes internas, o que é uma propriedade útil e também um limite a ser conhecido.

Pergunta de entrevistaQual a diferença entre let, run, apply e also?

As quatro diferem em duas coisas: como o objeto é recebido e o que a função devolve. let recebe o objeto como it e devolve o resultado do bloco. run usa this e devolve o resultado. apply usa this e devolve o próprio objeto, o que serve para configurar. also recebe it e devolve o próprio objeto, o que serve para efeitos colaterais, como log. Na prática, uso let para transformar valores anuláveis, apply para inicializar e also para registrar algo.

6. erros comuns e boas práticas

O primeiro erro é usar apply quando o resultado é um valor calculado, ou let quando o objeto é o que deveria voltar. O resultado é um tipo diferente do esperado, e o erro costuma aparecer longe da origem, numa variável que recebeu o valor errado. Antes de escrever a função, decida o que o bloco deve devolver.

O segundo erro é encadear escopos aninhados e perder a noção de qual this ou it está em uso. A correção é encurtar o bloco, dar nome a valores intermediários e, quando necessário, extrair uma função nomeada.

O terceiro erro é usar extensão para simular polimorfismo. Uma função fun Animal.emitirSom() com comportamento diferente por subtipo não faz o que parece, porque a escolha é feita pelo tipo declarado. Se o comportamento varia, use membro virtual ou uma sealed com when exaustivo, como na aula anterior.

O quarto erro é criar extensões que parecem membros de uma classe que vem de outro pacote ou biblioteca sem avisar. Quem lê o código não sabe de onde o método vem, e uma extensão com o mesmo nome de um membro do tipo real é sempre perdedora, porque o membro vence. Dê nomes que mostrem a origem quando houver risco de confusão.

A boa prática é usar extensão para enriquecer tipos que você não controla e para deixar leituras mais claras, e usar as funções de escopo para configurar e transformar com um propósito que se lê em uma palavra: apply configura, let transforma, also observa.

Vale citar que as funções de escopo são inline, e por isso o lambda que você passa a elas não vira um objeto alocado no caminho comum. Essa é a base da aula Funções inline e custo de lambdas da trilha, sobre funções inline e o custo de lambdas.

7. exercícios

Escreva uma função fun String.sanitizar(): String que remove espaços das pontas e converte para minúsculas, e use let para encadear as duas operações sem variável intermediária. Depois teste com uma string nula e confirme que o resultado é nulo.

Crie uma classe Pedido com var para cliente, itens e total, e configure um objeto recém-criado usando apply. Depois reescreva o mesmo trecho com run e compare a legibilidade das duas versões.

Declare open class Forma { open fun area() = 0.0 } e class Quadrado : Forma() com area sobrescrito. Crie uma extensão Forma.descricao() e outra Quadrado.descricao(), e confirme qual versão é chamada quando a variável é declarada como Forma e quando é declarada como Quadrado.

8. glossário

Função de escopo: função de biblioteca padrão que executa um bloco de código sobre um objeto, como let, run, with, apply e also.

Receptor: objeto ao qual uma função ou extensão se aplica, acessível dentro do corpo como this.

Função de extensão: função declarada fora de uma classe que pode ser chamada como se fosse um membro dela, sem alterar a classe original.

Despacho virtual: escolha, em tempo de execução, da implementação de um método pelo tipo real do objeto. As extensões não usam esse mecanismo.

Sombreamento (shadowing): situação em que um nome interno esconde um nome externo de mesmo identificador, comum quando this de um bloco esconde uma variável de fora.

Fontes

Consultadas em 20/10/2026.

A página oficial de funções de extensão foi consultada em 2026-10-20. Ela confirma que a escolha é estática, pelo tipo declarado do receptor. Para extensões declaradas como membro de outra classe, o receptor de despacho continua virtual. A página não descreve como o compilador gera o método estático, e essa parte vem do meu conhecimento e não foi conferida. O caráter inline foi conferido só em let, na documentação da API da biblioteca padrão. As outras funções de escopo ainda não foram conferidas uma a uma. O comportamento de escolha pelo tipo declarado e a mensagem de erro de membro privado foram verificados pela execução dos exemplos no kotlinc 2.1.21.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?