Conexão · Interrompida

Algo não carregou

Parte desta página não chegou até você. Recarregue para tentar novamente — se persistir, verifique sua conexão.

Pular para o conteúdo principal
Engineering9 min de leitura

Kotlin 2.4: As Três Mudanças que Moveram Minha Mão no Teclado

O Kotlin 2.4.0 chegou com um changelog extenso, mas apenas três recursos mudaram a forma como eu realmente digito: context parameters estáveis, explicit backing fields e (ainda atrás de uma flag) name-based destructuring. Este é o meu recorte de engenheiro backend, verificado contra o compilador 2.4.0, mais a remoção do K1 que tive que colocar no calendário.

Todos os Posts
2/4

O Kotlin 2.4.0 foi lançado este mês, e as notas de release são longas: context parameters estáveis, explicit backing fields, annotation use-site targets, API UUID estável, verificações de ordenação, suporte a Java 26 e a remoção do compilador K1, entre outros. Li a lista inteira. Depois, voltei para alguns dos meus próprios arquivos e os reescrevi contra o 2.4 para ver quais linhas realmente mudaram.

A resposta honesta: três recursos mudaram a forma como eu digito, e uma remoção mudou como eu organizei minha semana. Todo o resto é uma nota de rodapé que vou apreciar no dia em que esbarrar nela. Este é o recorte opinativo, escrito da cadeira de um engenheiro backend em vez de um Android.

Context parameters: passar o logger sem passar o logger

Este é o que eu vou buscar primeiro. Os context parameters se tornaram Estáveis no 2.4 (issue de rastreamento KT-72222), substituindo o experimento mais antigo de context receivers.

O problema que eles resolvem é a dependência que toda função em um caminho de chamada precisa, mas ninguém quer na assinatura. Um logger com escopo de requisição é o exemplo mais limpo. Um clock, um handle de transação ou um tenant id funcionam da mesma forma. Você ou passa o valor por toda função como um argumento que ela na verdade não usa, ou recorre a uma thread-local e perde toda a segurança de tipos.

Um context parameter é uma terceira opção: a função declara que precisa de um valor de algum tipo de seu entorno, e o compilador o fornece no call site sem você escrever o argumento.

Aqui está um arquivo autocontido. Ele compila e roda no Kotlin 2.4.0 sem flags.

kotlin
import java.util.UUID

// Uma coisa que todo handler precisa mas ninguém quer na assinatura:
// um logger com escopo de requisição que marca linhas com o request id.
class RequestLog(val requestId: UUID) {
    fun info(message: String) = println("[$requestId] $message")
}

data class Order(val id: String, val cents: Long)

// O context parameter diz: "Preciso de um RequestLog do meu entorno."
// Os chamadores nunca passam explicitamente; o compilador resolve por tipo.
context(log: RequestLog)
fun charge(order: Order): Long {
    log.info("charging order ${order.id} for ${order.cents}c")
    return order.cents
}

context(log: RequestLog)
fun refund(order: Order): Long {
    log.info("refunding order ${order.id}")
    return -order.cents
}

// Um único `with(log)` abre o escopo; tudo dentro vê o logger.
fun handleRequest(order: Order) {
    val log = RequestLog(UUID.nameUUIDFromBytes("req-42".toByteArray()))
    with(log) {
        val net = charge(order) + refund(order)
        log.info("net effect: ${net}c")
    }
}

fun main() {
    handleRequest(Order("A-100", 2599))
}

Execute com kotlinc orders.kt -include-runtime -d orders.jar && java -jar orders.jar. Ele imprime três linhas, cada uma marcada com o mesmo request id, e charge e refund nunca nomearam o logger na chamada.

A linha que vale estudar é context(log: RequestLog). O nome log torna a dependência referenciável dentro do corpo, algo que os context receivers sem nome não conseguiam fazer de forma limpa. No call site, charge(order) não carrega logger; o bloco with(log) colocou um RequestLog em escopo, e o compilador o casou por tipo.

Esse casamento é a parte que você precisa entender antes de se comprometer com o recurso. Pela minha leitura do código de resolução FIR, a resolução de contexto é uma busca real por escopo orientada por tipo, não um truque sintático. Para cada context parameter, o compilador conta os valores correspondentes em escopo e decide pela contagem.

O diagrama acima é todo o modelo mental. Zero valores do tipo certo em escopo, e a chamada reporta NoContextArgument e não compila. Exatamente um, e ele é passado. Dois ou mais, e você recebe AmbiguousContextArgument — o compilador se recusa a adivinhar entre dois loggers igualmente válidos. Essa é a proteção: uma dependência implícita que está faltando ou é ambígua é um erro de compilação, nunca uma escolha silenciosa em tempo de execução.

O trade-off é a legibilidade, e é real. Um leitor escaneando charge(order) não consegue ver que um logger está fluindo. Você trocou um argumento explícito-mas-barulhento por um invisível-mas-quieto. Minha regra ao reescrever um handler assim: context parameters valem a pena para valores transversais que genuinamente atravessam um grafo de chamadas profundo — um logger, um clock, uma transação. No momento em que dois valores do mesmo tipo podem coexistir em escopo, você está a um refactor de um erro de ambiguidade, e a implicitude para de compensar.

Uma ressalva específica para o 2.4: nomear o argumento no call site, como charge(log = primary), ainda é experimental atrás de -Xexplicit-context-arguments. O recurso de linguagem que liga isso por padrão está previsto para o 2.5. Então no 2.4 você tem a forma implícita estável, mas ainda não pode desambiguar por nome sem a flag opt-in.

Explicit backing fields aposentam a propriedade shadow privada

A segunda mudança que continuo usando é explicit backing fields, também Estável no 2.4. O número do tracker, KT-14663, é uma das requests abertas mais antigas no Kotlin, o que mostra quanto tempo o padrão que ele elimina existe.

O padrão é a propriedade que você expõe como um tipo read-only enquanto seu backing field guarda um tipo mais amplo e mutável. No Android é o par _state / state StateFlow, mas o formato aparece em muito código backend: um buffer de métricas, uma lista acumuladora, qualquer coisa internamente mutável e read-only para chamadores. O modo antigo precisa de duas declarações:

kotlin
private val _items = mutableListOf<Int>()
val items: List<Int> get() = _items

Dois nomes para um pedaço de estado, só porque o tipo interno e o tipo externo diferem. Explicit backing fields colapsam isso em uma única propriedade:

kotlin
class Counter {
    val total: List<Int>
        field = mutableListOf<Int>()

    fun add(n: Int) { total.add(n) }   // dentro: MutableList<Int>
}

Compilei e executei exatamente isso no 2.4.0. Dentro da classe, total resolve para MutableList<Int>, então total.add(n) funciona. Fora, total é List<Int> e não expõe nenhuma mutação. Uma propriedade, duas visibilidades do mesmo objeto, sem propriedade shadow prefixada com _.

As restrições valem conhecer antes de buscar o recurso. Pela minha leitura do checker, um explicit backing field é permitido apenas em um val final, nunca em um var, propriedade open, ou em propriedades de interface, abstratas, expect ou extension. A visibilidade do backing field deve ser mais restritiva do que a da propriedade. A lógica é consistência: se a propriedade pudesse ser sobrescrita, uma subclasse poderia fornecer um tipo de campo diferente, e um chamador não conseguiria mais raciocinar sobre qual tipo ela carrega. A restrição a val final remove essa ambiguidade. Para código backend isso aterrissa menos frequentemente do que para um Android ViewModel, mas toda vez que tenho uma propriedade "mutável dentro, imutável fora", a shadow agora se foi.

Name-based destructuring, ainda atrás de uma flag

A terceira mudança não foi lançada como estável, e quero ser preciso sobre isso porque é fácil interpretar mal a cobertura da release. Name-based destructuring é experimental no 2.4, atrás de -Xname-based-destructuring. Estou incluindo porque ela já mudou como eu escrevo novos consumidores de data class em branches em que estou disposto a usar feature flag, e porque corrige uma classe de bug que já vivi de verdade.

O destructuring posicional vincula por ordem, não por nome. A armadilha clássica:

kotlin
data class Person(val name: String, val age: Int)
val (age, name) = person   // compila, ambos estão errados

Isso compila. age recebe o nome e name recebe a idade, porque posição é tudo o que o compilador usa. Adicione um campo no meio da data class depois, e todo destructuring posicional downstream silenciosamente se desloca.

Name-based destructuring vincula pelo nome da propriedade. Verifiquei isso no 2.4.0 com -Xname-based-destructuring=only-syntax:

kotlin
(val age, val name) = person   // vincula por nome, ordem irrelevante

Aqui age recebe person.age e name recebe person.name, independentemente da ordem em que escrevi ou da ordem em que a data class declara. O bytecode é inalterado; este é um recurso do call site. A flag tem três modos que vale conhecer: only-syntax habilita a nova forma parentizada, name-mismatch avisa quando um destructuring posicional antigo usa nomes que não combinam com as propriedades, e complete liga a forma curta. A JetBrains sinalizou estabilização para uma release posterior.

Ainda não estou colocando isso no main. Mas só o modo name-mismatch vale um experimento de CI: ele expõe todo destructuring posicional existente em que os nomes das variáveis já mentem sobre qual campo elas carregam.

A remoção do K1 que tive que agendar

A mudança breaking no 2.4 é estrutural: o frontend do compilador K1 sumiu. Desde o 2.0, o K2 é o padrão, mas o K1 persistia atrás de -language-version 1.9 para projetos que precisavam dele. No 2.4 essa saída de emergência foi removida. O compilador agora rejeita -language-version 1.9 diretamente; a versão de linguagem mais baixa que aceita é 2.0.

Se seu build ainda fixa languageVersion = "1.9" em algum lugar — uma convenção Gradle, um módulo teimoso, defaults de um plugin de terceiros — o 2.4 não vai compilá-lo. Esse é o item de linha que tive que colocar no calendário em vez de absorver no dia do upgrade. A correção não é difícil, mas não é automática: encontre toda versão de linguagem fixada, abaixe para pelo menos 2.0, e recompile contra a resolução mais estrita do K2, que pega algumas construções que o K1 deixava passar.

Para usuários de Kotlin Multiplatform há uma mudança pareada: partial linkage agora está permanentemente ativo. A flag -Xpartial-linkage está depreciada, e apenas -Xpartial-linkage-loglevel sobrevive para controlar quão barulhento um link degradado fica. Se você dependia de desligar partial linkage, esse switch sumiu.

O restante das adições da biblioteca padrão do 2.4 são as notas de rodapé que mencionei. isSorted, isSortedBy e isSortedDescending chegam em iteráveis, arrays e sequences; eles fazem short-circuit no primeiro par fora de ordem em vez de varrer a coleção inteira. O tipo kotlin.uuid.Uuid agora está estável, então parsear e formatar UUIDs não precisa de opt-in — embora generateV4 e generateV7 para criar novos ainda sejam experimentais. UInt.toBigInteger() e ULong.toBigInteger() substituem as antigas conversões baseadas em string na JVM. Bom de ter, nenhum deles mudou uma linha que eu já tinha escrito.

O que realmente fazer com isso

  • Busque context parameters para um valor transversal que atravessa um grafo de chamadas profundo — logger, clock, transação. Pare no momento em que dois valores do mesmo tipo possam compartilhar um escopo, ou você trocará código quieto por um erro AmbiguousContextArgument.
  • Use explicit backing fields onde quer que tenha um val final que é mutável dentro e read-only fora. Apague a propriedade shadow prefixada com _. Lembre-se de que é apenas para val final.
  • Experimente name-based destructuring no modo name-mismatch em CI antes de adotar. Ele sinaliza todo destructuring posicional cujos nomes de variáveis já discordam da data class.
  • Antes de atualizar, faça grep por languageVersion = "1.9" em todo o seu build e abaixe para 2.0+. O K1 sumiu; um 1.9 fixado quebra o build.

Quando evitar cada um: pule context parameters para dependências pontuais que uma única função precisa — um argumento simples é mais claro. Pule explicit backing fields em propriedades var ou open; o compilador não vai deixar de qualquer forma. Mantenha name-based destructuring fora do main até estabilizar. E não trate a remoção do K1 como opcional — é a única mudança no 2.4 que falha de forma fechada.

Continue lendo

Curtindo? Talvez goste disso aqui.

Nada parecido — quer tentar outro ângulo?

Isso foi útil?

Deixe uma avaliação ou uma nota rápida — me ajuda a melhorar.