O que é Receiver em Kotlin?

O receiver é o objeto sobre o qual uma função opera, acessivel dentro da função através da palavra-chave this. Em Kotlin, o conceito de receiver aparece em extension functions, lambdas with receiver e scope functions, sendo fundamental para a criação de APIs fluentes e DSLs.

Em uma função normal, os parametros são explicitamente nomeados. Com um receiver, o objeto esta implicito no escopo da função, permitindo acessar suas propriedades e métodos diretamente sem qualificacao.

Receiver em extension functions

O exemplo mais comum de receiver e em extension functions:

fun String.contarVogais(): Int {
    // 'this' se refere ao String sobre o qual a funcao foi chamada
    return this.count { it in "aeiouAEIOU" }
}

fun main() {
    val texto = "Kotlin Brasil"
    println(texto.contarVogais()) // 5
}

Aqui, String e o tipo do receiver. Dentro da função, this aponta para a instancia de String sobre a qual contarVogais() foi chamado. O this pode ser omitido:

fun String.contarVogais(): Int {
    return count { it in "aeiouAEIOU" } // 'this' implicito
}

Lambda with receiver

Uma lambda with receiver e uma lambda que tem acesso ao receiver como this. A sintaxe do tipo e TipoReceiver.() -> TipoRetorno:

fun construirMensagem(builder: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.builder() // Chama a lambda com sb como receiver
    return sb.toString()
}

fun main() {
    val mensagem = construirMensagem {
        // Dentro dessa lambda, 'this' e o StringBuilder
        append("Ola, ")
        append("Kotlin!")
        appendLine()
        append("Tudo bem?")
    }
    println(mensagem)
}

A lambda builder tem StringBuilder como receiver, entao dentro dela você pode chamar append, appendLine e outros métodos diretamente, como se estivesse dentro de um método da classe.

Scope functions e receivers

As scope functions do Kotlin usam diferentes tipos de receiver:

data class Pessoa(var nome: String, var idade: Int)

fun main() {
    val pessoa = Pessoa("Ana", 25)

    // apply: receiver e 'this', retorna o objeto
    pessoa.apply {
        nome = "Ana Maria"  // this.nome
        idade = 26           // this.idade
    }

    // with: receiver e 'this', retorna o resultado da lambda
    val descricao = with(pessoa) {
        "$nome tem $idade anos" // this.nome, this.idade
    }

    // run: receiver e 'this', retorna o resultado da lambda
    val resultado = pessoa.run {
        "$nome ($idade)"
    }

    // let: parametro e 'it', nao usa receiver
    pessoa.let { p ->
        println("${p.nome} tem ${p.idade} anos")
    }

    // also: parametro e 'it', retorna o objeto
    pessoa.also {
        println("Pessoa: ${it.nome}")
    }
}

apply, with e run usam lambda with receiver (this). let e also usam lambda com parametro (it).

Criando DSLs com receiver

O poder real dos receivers aparece na criação de DSLs (Domain Specific Languages):

class HtmlBuilder {
    private val elementos = mutableListOf<String>()

    fun h1(texto: String) {
        elementos.add("<h1>$texto</h1>")
    }

    fun p(texto: String) {
        elementos.add("<p>$texto</p>")
    }

    fun ul(configurar: ListaBuilder.() -> Unit) {
        val builder = ListaBuilder()
        builder.configurar()
        elementos.add(builder.construir())
    }

    fun construir(): String = elementos.joinToString("\n")
}

class ListaBuilder {
    private val itens = mutableListOf<String>()

    fun li(texto: String) {
        itens.add("<li>$texto</li>")
    }

    fun construir(): String {
        val conteudo = itens.joinToString("\n  ")
        return "<ul>\n  $conteudo\n</ul>"
    }
}

fun html(configurar: HtmlBuilder.() -> Unit): String {
    val builder = HtmlBuilder()
    builder.configurar()
    return builder.construir()
}

fun main() {
    val pagina = html {
        h1("Kotlin Brasil")
        p("Aprenda Kotlin em português.")
        ul {
            li("Coroutines")
            li("Flow")
            li("Compose")
        }
    }
    println(pagina)
}

Cada bloco { } tem um receiver diferente, e os métodos disponiveis mudam conforme o contexto. Isso cria uma sintaxe que parece uma linguagem própria.

@DslMarker para controle de escopo

Sem controle, receivers de escopos externos podem vazar para escopos internos:

@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HtmlBuilder { /* ... */ }

@HtmlDsl
class ListaBuilder { /* ... */ }

// Agora, dentro de ul { }, voce NAO pode acessar h1() ou p()
// Isso impede erros de escopo na DSL

O @DslMarker restringe o acesso a receivers de escopos externos, tornando a DSL mais segura e previsivel.

Receiver em funções de extensao de tipos genericos

fun <T> T.tambem(bloco: (T) -> Unit): T {
    bloco(this)
    return this
}

fun <T> T.executar(bloco: T.() -> Unit): T {
    this.bloco()
    return this
}

A diferenca entre (T) -> Unit (parametro) e T.() -> Unit (receiver) e sutil mas importante:

// Com parametro: acesso via 'it' ou nome do parametro
"texto".tambem { valor -> println(valor.length) }

// Com receiver: acesso via 'this' (implicito)
"texto".executar { println(length) }

Múltiplos receivers

Kotlin permite que uma função tenha um receiver de extensao e um dispatch receiver (da classe):

class Logger {
    fun String.logInfo() {
        // 'this' se refere ao String (extension receiver)
        // 'this@Logger' se refere ao Logger (dispatch receiver)
        println("[INFO] $this")
    }
}

fun main() {
    val logger = Logger()
    with(logger) {
        "Mensagem de teste".logInfo() // [INFO] Mensagem de teste
    }
}

Quando usar receivers

  • Extension functions: para adicionar funcionalidade a tipos existentes sem heranca.
  • DSLs: para criar APIs que leem como linguagem natural.
  • Builders: para construir objetos complexos com sintaxe fluente.
  • Scope functions: para operações rápidas em objetos sem criar variaveis intermediarias.
  • Configuração: para blocos de configuração onde o contexto e claro e os métodos disponiveis são limitados.

Casos de Uso no Mundo Real

  1. DSLs de construcao de UI: frameworks como Jetpack Compose e bibliotecas de HTML (kotlinx.html) usam lambdas with receiver extensivamente para criar interfaces declarativas. Cada bloco de construcao (Column, Row, Box) recebe uma lambda cujo receiver e o escopo de layout correspondente, permitindo que modificadores e filhos sejam adicionados de forma natural.

  2. configuração de clientes HTTP: bibliotecas como Ktor usam receivers para configurar requisicoes HTTP de forma fluente. O bloco de configuração do cliente, rotas, autenticação e plugins usam lambdas with receiver, permitindo que cada nivel de configuração exponha apenas os métodos relevantes ao contexto.

  3. Builders de consultas de banco de dados: ORMs como Exposed usam receivers para construir queries SQL de forma type-safe. Dentro de um bloco transaction, o receiver fornece acesso a operações de banco; dentro de select, o receiver muda para expor operações de filtragem e projecao.

  4. Extension functions em projetos corporativos: equipes adicionam funções de extensao a classes de dominio para encapsular lógica recorrente (formatacao, validacao, conversao) sem alterar as classes originais, especialmente útil quando as classes vem de bibliotecas de terceiros.

Boas Praticas

  • Use @DslMarker ao criar DSLs com receivers aninhados: sem essa anotacao, receivers de escopos externos vazam para escopos internos, permitindo chamadas de métodos que não fazem sentido no contexto atual e gerando bugs sutis.
  • Prefira this explicito em escopos ambiguos: quando há múltiplos receivers em escopos aninhados, usar this@NomeDaFuncao torna o código mais claro e evita que o receiver errado seja usado acidentalmente.
  • Nao polua tipos comuns com muitas extension functions: adicionar dezenas de extensoes a String, Int ou List dificulta a descoberta de funcionalidade e pode gerar conflitos de nome. Agrupe extensoes relacionadas em arquivos específicos e com boa documentação.
  • Use lambda with receiver para APIs de configuração: quando sua função recebe um bloco de configuração, declarar o parametro como Config.() -> Unit em vez de (Config) -> Unit torna o código do chamador mais limpo e idiomatico.
  • Evite extension functions com efeitos colaterais inesperados: uma função String.salvarNoBanco() viola o princípio de menor surpresa. Extension functions devem ter comportamento previsivel e coerente com o tipo que estendem.

Perguntas Frequentes

P: Extension functions podem acessar membros privados da classe que estendem? R: Nao. Extension functions tem acesso apenas a membros publicos e internal do tipo receptor. Elas sao resolvidas estaticamente e não alteram a classe original. Se você precisa acessar membros privados, use uma função membro da própria classe.

P: Qual a diferenca entre T.() -> Unit e (T) -> Unit? R: T.() -> Unit e uma lambda with receiver, onde dentro da lambda você acessa T via this (implicito). (T) -> Unit e uma lambda com parametro, onde você acessa T via nome do parametro ou it. Funcionalmente sao equivalentes, mas a versão com receiver produz código mais limpo em contextos de builder e DSL.

P: O que sao Context Receivers e como se relacionam com receivers normais? R: Context Receivers sao um recurso experimental do Kotlin que permite declarar múltiplos receivers implicitos para uma função, sem precisar encadear extension functions. Enquanto um receiver normal e único (fun String.algo()), context receivers permitem que a função exija múltiplos contextos simultaneamente (context(Logger, Database) fun processar()).

P: Extension functions sao resolvidas em tempo de compilação ou execução? R: Em tempo de compilação (dispatch estático). Isso significa que se uma classe filha e uma classe pai ambas tem uma extension function com a mesma assinatura, a função chamada depende do tipo declarado da variavel, não do tipo real do objeto em tempo de execução.

Erros comuns

  1. Receiver errado em escopos aninhados: quando você tem lambdas aninhadas com diferentes receivers, this se refere ao mais interno. Use this@NomeDaFuncao para acessar receivers externos.

  2. Abusar de extension functions: adicionar muitas extensions a tipos comuns como String ou Int polui o namespace e dificulta a descoberta de funcionalidade.

  3. Nao usar @DslMarker: sem essa anotacao, DSLs permitem chamadas de métodos de escopos externos, levando a erros sutis.

  4. Confundir T.() -> Unit com (T) -> Unit: a primeira e uma lambda with receiver (acesso via this); a segunda e uma lambda com parametro (acesso via nome do parametro ou it).

  5. Esquecer que extension functions não tem acesso a membros privados: o receiver this da acesso apenas a membros publicos e internos do tipo, não a membros privados ou protegidos.

Termos relacionados

  • Extension Function: função que adiciona métodos a um tipo usando receiver.
  • Context Receiver: recurso experimental que permite múltiplos receivers implícitos.
  • Scope Functions: let, run, with, apply, also – funções que manipulam receivers.
  • DSL: Domain Specific Language, construida usando lambdas with receiver.
  • this: palavra-chave que referência o receiver atual.
  • Lambda: expressao funcional que pode ter receiver (T.() -> Unit).

Receivers são um dos conceitos mais poderosos e distintos do Kotlin. Eles permitem que o código expresse intencao de forma clara e concisa, transformando operações complexas em blocos legiveis que parecem parte da linguagem. Dominar receivers e dominar a essencia do Kotlin idiomatico.