Inline functions y reified generics

Avanzado
Kotlin
Kotlin
Actualizado: 04/05/2026

Diagrama: tutorial-kotlin-inline-functions-reified

Las inline functions son funciones cuyo cuerpo se copia en el lugar de la llamada durante la compilación. Para un lenguaje que apoya fuertemente la programación funcional, este comportamiento es clave: permite escribir funciones de orden superior sin pagar el coste de crear objetos para las lambdas ni capturar variables en closures.

Además, el inlining desbloquea una caracteristica única en la JVM: los generics reificados. Normalmente, un parámetro de tipo se borra en tiempo de ejecución (type erasure). Una función inline con un tipo marcado reified conserva esa información y permite operar sobre ella como si fuera una clase concreta.

Lambdas y coste de asignación

En Kotlin, una lambda es un objeto que implementa una interfaz funcional. Sin inlining, cada invocación crea una instancia de esa interfaz y, si captura variables del entorno, también un objeto para la clausura.

fun medirSinInline(accion: () -> Unit): Long {
    val inicio = System.nanoTime()
    accion()
    return System.nanoTime() - inicio
}

En bucles calientes, estas asignaciones se acumulan. El inlining resuelve el problema porque el cuerpo de medirSinInline se copia donde se llama, y la lambda desaparece como tal.

inline fun medir(accion: () -> Unit): Long {
    val inicio = System.nanoTime()
    accion()
    return System.nanoTime() - inicio
}

fun main() {
    val tiempo = medir {
        var suma = 0L
        repeat(1_000_000) { suma += it }
    }
    println("Duracion: ${tiempo / 1_000_000} ms")
}

En el bytecode generado, el cuerpo de medir se pega donde se invoca y el repeat se resuelve sin objetos auxiliares. Esta optimización es la que usan funciones de la biblioteca estandar como repeat, synchronized, use o las scope functions (let, run, with, apply, also), todas ellas declaradas como inline.

El inlining no es gratuito: el bytecode resultante crece. Se recomienda reservar inline para funciones pequenas con parámetros lambda, no para rutinas grandes.

Retornos no locales

Una de las ventajas directas del inlining es que la lambda puede ejecutar un return que escape de la función envolvente. Esto se llama retorno no local y no es posible con lambdas que se ejecuten fuera del contexto de compilación.

fun buscarPrimeroPar(numeros: List<Int>): Int? {
    numeros.forEach {
        if (it % 2 == 0) return it
    }
    return null
}

Como forEach es inline, el return it sale directamente de buscarPrimeroPar. Sin inlining, habría que usar una etiqueta (return@forEach) que solo sale del lambda.

noinline para lambdas que no se deben inlinear

Si una función inline recibe varias lambdas y alguna debe guardarse en una variable, pasarse a otra función o ejecutarse en otro hilo, no puede inlinearse. En esos casos se marca con noinline.

inline fun ejecutar(
    antes: () -> Unit,
    noinline despues: () -> Unit
): () -> Unit {
    antes()
    return despues
}

fun main() {
    val callback = ejecutar(
        antes = { println("Antes") },
        despues = { println("Despues en el callback") }
    )
    callback()
}

La lambda despues se guarda como un objeto real y se devuelve al llamador. La lambda antes, en cambio, se integra en el flujo de la función. La mezcla de inline y noinline permite granularidad fina sobre el coste de cada parámetro.

crossinline para lambdas que no pueden usar retorno no local

Hay situaciones en las que la lambda se invoca en un contexto diferente al de la llamada original, por ejemplo dentro de un object anónimo, un handler o una coroutine. En esos casos un return no local causaria un desvio del flujo imposible. La palabra clave crossinline avisa al compilador de esta restricción.

inline fun registrarBoton(crossinline onClick: () -> Unit) {
    val listener = object : Runnable {
        override fun run() {
            onClick()
        }
    }
    listener.run()
}

fun main() {
    registrarBoton {
        println("Click procesado")
    }
}

Si dentro de la lambda se intenta un return que escape, el compilador lo rechaza. crossinline conserva la eficiencia del inlining pero restringe el uso de retornos no locales, que no tendrian sentido al ejecutarse en otro objeto.

Reified generics

El borrado de tipos de la JVM impide comprobar si un valor es de un tipo genérico. Expresiones como x is T fallan cuando T es un parámetro. Las funciones inline con parámetros reified rompen esa barrera porque, al copiar el cuerpo en el sitio de la llamada, el tipo concreto es visible.

inline fun <reified T> filtrarPorTipo(items: List<Any>): List<T> {
    return items.filterIsInstance<T>()
}

fun main() {
    val datos: List<Any> = listOf(1, "dos", 3, 4.0, "cinco")
    val soloCadenas = filtrarPorTipo<String>(datos)
    val soloEnteros = filtrarPorTipo<Int>(datos)
    println(soloCadenas)
    println(soloEnteros)
}

La función se llama sin pasar la clase como parámetro. El compilador inyecta el String::class.java o Int::class.java al generar cada invocación.

Acceso a la KClass del tipo reificado

inline fun <reified T> nombreDeTipo(): String = T::class.simpleName ?: "anonimo"

fun main() {
    println(nombreDeTipo<Int>())
    println(nombreDeTipo<List<String>>())
}

Dentro del cuerpo, T::class representa la clase real. Esta combinación habilita patrones como parseo de JSON a un tipo genérico sin pasar manualmente la clase.

import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.EmptySerializersModule

inline fun <reified T> Json.parsear(texto: String): T =
    decodeFromString(serializersModule.serializer(), texto) as T

Este patrón es el que utilizan frameworks como Ktor para el cliente HTTP, donde client.get<Usuario>(...) decodifica la respuesta en el tipo solicitado sin que el llamante pase Usuario::class.

El patrón es asimetrico: reified solo existe dentro del contexto de una función inline, porque requiere que el tipo concreto viaje con la llamada. Al copiarlo en el sitio de uso, el compilador sabe que tipo poner.

Ejemplo: factory genérica

class Contenedor {
    private val servicios = mutableMapOf<Class<*>, Any>()

    fun <T : Any> registrar(tipo: Class<T>, instancia: T) {
        servicios[tipo] = instancia
    }

    fun <T : Any> obtener(tipo: Class<T>): T {
        @Suppress("UNCHECKED_CAST")
        return servicios[tipo] as T
    }
}

inline fun <reified T : Any> Contenedor.registrar(instancia: T) = registrar(T::class.java, instancia)

inline fun <reified T : Any> Contenedor.obtener(): T = obtener(T::class.java)

class ServicioCorreo
class ServicioPago

fun main() {
    val c = Contenedor()
    c.registrar(ServicioCorreo())
    c.registrar(ServicioPago())

    val correo = c.obtener<ServicioCorreo>()
    val pago = c.obtener<ServicioPago>()
    println("${correo::class.simpleName}, ${pago::class.simpleName}")
}

La API expuesta a los consumidores no tiene que pasar ::class.java manualmente. Las extensiones reified convierten las llamadas en c.registrar(Servicio()) y c.obtener<Servicio>(), formas mucho mas limpias que dependen por completo del mecanismo de inlining.

Cuando conviene aplicarlo

El inlining es una herramienta quirurgica, no una optimización general. Estas pautas ayudan a usarla donde aporta.

  • 1. Funciones con uno o mas parámetros lambda: son el caso principal que justifica inline.
  • 2. APIs orientadas a generics con acceso al tipo: ahi es donde reified despliega todo su valor.
  • 3. DSLs y builders: librerias como kotlinx.html o Compose usan inlining para mantener la fluidez.
  • 4. Evita inlinear funciones sin lambdas: el compilador ya avisa que el inline no aporta y sugiere quitarlo.
  • 5. Evita inlinear funciones grandes: hacen crecer el bytecode de cada llamador.

Al escribir código de alto nivel con funciones de orden superior, aplicar inline en las utilidades mas usadas puede marcar la diferencia en bucles calientes. En la mayoria de aplicaciones de negocio, el efecto visible es mas bien la limpieza de la API y la posibilidad de usar reified para tipar operaciones genéricas con naturalidad.

inline aplicado a propiedades

Además de funciones, se pueden marcar como inline propiedades sin backing field. El getter se copia en el sitio de la llamada igual que con las funciones. Es útil para accesos calculados que se consumen en bucles frecuentes.

class Rango(val inicio: Int, val fin: Int) {
    inline val longitud: Int get() = fin - inicio + 1
    inline val esVacio: Boolean get() = fin < inicio
}

fun main() {
    val r = Rango(5, 12)
    println("longitud=${r.longitud}, vacio=${r.esVacio}")
}

La anotación debe aplicarse con moderación. Con un solo acceso la ventaja es nula, pero dentro de un bucle que evalua miles de rangos puede ahorrar la llamada a un getter.

Comparativa con alternativas

Una pregunta habitual es cuando usar inline frente a otras herramientas del lenguaje. La respuesta depende del problema que se quiera resolver.

  • Sustituir inline por constantes: cuando la función no recibe lambdas, a menudo una función normal o incluso una constante es mas clara.
  • Sustituir reified por paso explícito de KClass: en APIs públicas consumidas desde Java, pasar Usuario::class.java es mas portable.
  • Evitar inline en funciones muy usadas en interfaces: las interfaces no pueden declarar métodos inline. Si el mecanismo se expone via interfaz, se pierde la ventaja.

La biblioteca estandar es una buena guía: run, let, with, apply, also, repeat, synchronized, use, measureTimeMillis y muchas funciones de colecciones (forEach, map, filter) estan declaradas como inline. Cuando el consumidor las combina con lambdas cortas, el compilador genera código tan eficiente como un bucle escrito a mano.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Kotlin es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Kotlin

Explora más contenido relacionado con Kotlin y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Usar inline functions para eliminar el coste de lambdas, aplicar reified generics para acceder al tipo genérico en tiempo de ejecución y combinar noinline y crossinline según las restricciones del compilador.