Imagine poder adicionar métodos novos a classes que já existem — sem herança, sem wrappers, sem modificar o código original. Parece mágica? Em Kotlin, isso se chama Extension Functions, e é um dos recursos mais queridos da linguagem.

O que são Extension Functions?

Extension Functions permitem que você “estenda” uma classe com novas funções, como se elas fizessem parte da classe original. A sintaxe é simples:

fun String.contarPalavras(): Int {
    return this.trim().split("\\s+".toRegex()).size
}

fun main() {
    val frase = "Kotlin é incrível demais da conta"
    println(frase.contarPalavras()) // 6
}

Repare: contarPalavras() não existe na classe String, mas a gente chamou como se existisse. O this dentro da função refere-se ao objeto receptor (no caso, a String).

Exemplos práticos do dia a dia

Vamos ver extensões que você vai querer copiar agora mesmo pro seu projeto:

Formatação de valores brasileiros

fun Double.toBRL(): String {
    return "R$ %,.2f".format(this).replace(",", "X").replace(".", ",").replace("X", ".")
}

fun String.toCpfFormatado(): String {
    val limpo = this.replace("[^0-9]".toRegex(), "")
    require(limpo.length == 11) { "CPF deve ter 11 dígitos" }
    return "${limpo.substring(0,3)}.${limpo.substring(3,6)}.${limpo.substring(6,9)}-${limpo.substring(9)}"
}

fun String.toCnpjFormatado(): String {
    val limpo = this.replace("[^0-9]".toRegex(), "")
    require(limpo.length == 14) { "CNPJ deve ter 14 dígitos" }
    return "${limpo.substring(0,2)}.${limpo.substring(2,5)}.${limpo.substring(5,8)}/${limpo.substring(8,12)}-${limpo.substring(12)}"
}

fun main() {
    println(1499.90.toBRL())                     // R$ 1.499,90
    println("12345678901".toCpfFormatado())       // 123.456.789-01
    println("12345678000190".toCnpjFormatado())   // 12.345.678/0001-90
}

Validações

fun String.isEmailValido(): Boolean {
    return matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}".toRegex())
}

fun String.isCpfValido(): Boolean {
    val numeros = this.replace("[^0-9]".toRegex(), "")
    if (numeros.length != 11) return false
    if (numeros.all { it == numeros[0] }) return false

    // Validação dos dígitos verificadores
    val dv1 = (0..8).sumOf { (10 - it) * numeros[it].digitToInt() } % 11
    val primeiroDigito = if (dv1 < 2) 0 else 11 - dv1

    val dv2 = (0..9).sumOf { (11 - it) * numeros[it].digitToInt() } % 11
    val segundoDigito = if (dv2 < 2) 0 else 11 - dv2

    return numeros[9].digitToInt() == primeiroDigito &&
           numeros[10].digitToInt() == segundoDigito
}

fun main() {
    println("karina@kotlin.dev.br".isEmailValido()) // true
    println("email-invalido".isEmailValido())        // false
}

Manipulação de datas

import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period

fun LocalDate.toBrazilianFormat(): String {
    return this.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
}

fun LocalDate.idadeEmAnos(): Int {
    return Period.between(this, LocalDate.now()).years
}

fun main() {
    val nascimento = LocalDate.of(1995, 6, 15)
    println(nascimento.toBrazilianFormat()) // 15/06/1995
    println("Idade: ${nascimento.idadeEmAnos()} anos")
}

Extension Properties

Não é só função — você também pode criar propriedades de extensão:

val String.primeiraLetra: Char
    get() = this.first()

val String.ultimaLetra: Char
    get() = this.last()

val List<*>.penultimoElemento: Any?
    get() = this[this.size - 2]

fun main() {
    println("Kotlin".primeiraLetra)  // K
    println("Kotlin".ultimaLetra)    // n
}

Uma limitação importante: extension properties não podem ter backing field, ou seja, não podem armazenar estado. Elas sempre precisam ser calculadas.

Extensions em coleções

Extensões brilham quando trabalham com coleções:

fun <T> List<T>.segundoOuNull(): T? = if (this.size >= 2) this[1] else null

fun List<Int>.mediana(): Double {
    val ordenada = this.sorted()
    val meio = ordenada.size / 2
    return if (ordenada.size % 2 == 0) {
        (ordenada[meio - 1] + ordenada[meio]) / 2.0
    } else {
        ordenada[meio].toDouble()
    }
}

fun <T> List<T>.agruparEmPares(): List<Pair<T, T?>> {
    return this.chunked(2).map { chunk ->
        Pair(chunk[0], chunk.getOrNull(1))
    }
}

fun main() {
    val numeros = listOf(3, 1, 4, 1, 5, 9, 2, 6)
    println(numeros.mediana())          // 3.5
    println(numeros.segundoOuNull())    // 1
    println(numeros.agruparEmPares())   // [(3, 1), (4, 1), (5, 9), (2, 6)]
}

Como funciona por baixo dos panos

É importante entender: extension functions não modificam realmente a classe. Elas são resolvidas estaticamente. No bytecode, uma extension function vira uma função estática onde o primeiro parâmetro é o objeto receptor:

// Isso:
fun String.gritando() = this.uppercase() + "!!!"

// Vira essencialmente isso no bytecode:
// public static String gritando(String $this) { return $this.toUpperCase() + "!!!"; }

Isso tem implicações importantes:

  • Não há polimorfismo: se uma classe filha e uma classe pai têm extensions com o mesmo nome, o tipo declarado (não o real) determina qual é chamada
  • Membros sempre ganham: se a classe já tem um método com o mesmo nome, o membro da classe vence
class Mensagem(val texto: String) {
    fun exibir() = "Classe: $texto"
}

fun Mensagem.exibir() = "Extension: $texto" // NUNCA será chamada

fun main() {
    println(Mensagem("Oi").exibir()) // "Classe: Oi" — o membro vence
}

Organizando extensions

Em projetos maiores, organize suas extensions em arquivos dedicados:

src/
  extensions/
    StringExtensions.kt
    DateExtensions.kt
    CollectionExtensions.kt
    ViewExtensions.kt  // Android
// StringExtensions.kt
package br.dev.kotlin.extensions

fun String.capitalizar(): String {
    return this.split(" ").joinToString(" ") { palavra ->
        palavra.replaceFirstChar { it.uppercase() }
    }
}

Boas práticas

  1. Use extensions para adicionar funcionalidade coesa a uma classe
  2. Não abuse: se a lógica é complexa, talvez uma classe própria faça mais sentido
  3. Dê nomes descritivos: a extension deve parecer um membro natural da classe
  4. Documente: extensions podem surpreender quem lê o código pela primeira vez
  5. Agrupe por tipo: mantenha as extensions organizadas em arquivos temáticos

Conclusão

Extension Functions são um dos recursos que fazem Kotlin ser tão expressivo e prazeroso de programar. Elas permitem que você adapte a linguagem ao seu domínio, criando uma API fluente e natural. Use com sabedoria, e seu código vai ficar muito mais legível e manutenível.

Bora estender tudo!