¿Cuándo usar una función en línea en Kotlin?

105

Sé que una función en línea tal vez mejore el rendimiento y haga que el código generado crezca, pero no estoy seguro de cuándo es correcto usar una.

lock(l) { foo() }

En lugar de crear un objeto de función para el parámetro y generar una llamada, el compilador podría emitir el siguiente código. ( Fuente )

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

pero encontré que no hay ningún objeto de función creado por kotlin para una función no en línea. ¿por qué?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
holi-java
fuente
7
Hay dos casos de uso principales para esto, uno es con ciertos tipos de funciones de orden superior y el otro son parámetros de tipo reificado. La documentación de las funciones en línea cubre estas: kotlinlang.org/docs/reference/inline-functions.html
zsmb13
2
@ zsmb13 gracias, señor. pero no lo entiendo: "En lugar de crear un objeto de función para el parámetro y generar una llamada, el compilador podría emitir el siguiente código"
holi-java
2
Tampoco entiendo ese ejemplo tbh.
filthy_wizard

Respuestas:

279

Digamos que crea una función de orden superior que toma una lambda de tipo () -> Unit(sin parámetros, sin valor de retorno) y la ejecuta así:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

En el lenguaje de Java, esto se traducirá en algo como esto (¡simplificado!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

Y cuando lo llamas desde Kotlin ...

nonInlined {
    println("do something here")
}

Debajo del capó, se Functioncreará una instancia de aquí, que envuelve el código dentro de la lambda (nuevamente, esto está simplificado):

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

Básicamente, llamar a esta función y pasarle un lambda siempre creará una instancia de un Functionobjeto.


Por otro lado, si usa la inlinepalabra clave:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Cuando lo llamas así:

inlined {
    println("do something here")
}

No se Functioncreará ninguna instancia, en cambio, el código alrededor de la invocación blockdentro de la función en línea se copiará en el sitio de la llamada, por lo que obtendrá algo como esto en el código de bytes:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

En este caso, no se crean nuevas instancias.

zsmb13
fuente
19
¿Cuál es la ventaja de tener el contenedor de objetos Function en primer lugar? es decir, ¿por qué no está todo alineado?
Arturs Vancans
14
De esta manera, también puede pasar funciones arbitrariamente como parámetros, almacenarlas en variables, etc.
zsmb13
6
Gran explicación de @ zsmb13
Yajairo87
2
Puede, y si hace cosas complicadas con ellos, eventualmente querrá saber sobre las palabras clave noinliney crossinline; consulte los documentos .
zsmb13
2
Los documentos dan una razón por la que no querría insertar
CorayThan
43

Permítanme agregar: "Cuándo no usar inline" :

1) Si tiene una función simple que no acepta otras funciones como argumento, no tiene sentido alinearlas. IntelliJ le advertirá:

El impacto esperado en el rendimiento de insertar '...' es insignificante. Inlining funciona mejor para funciones con parámetros de tipos funcionales

2) Incluso si tiene una función "con parámetros de tipos funcionales", es posible que el compilador le diga que la inserción no funciona. Considere este ejemplo:

inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
    val o = operation //compiler does not like this
    return o(param)
}

Este código no se compilará con el error:

Uso ilegal del parámetro en línea 'operación' en '...'. Agregue el modificador 'noinline' a la declaración del parámetro.

La razón es que el compilador no puede insertar este código. Si operationno está envuelto en un objeto (lo que está implícito en inlineya que desea evitar esto), ¿cómo se puede asignar a una variable? En este caso, el compilador sugiere hacer el argumento noinline. Tener una inlinefunción con una sola noinlinefunción no tiene ningún sentido, no hagas eso. Sin embargo, si hay varios parámetros de tipos funcionales, considere incluir algunos de ellos si es necesario.

Así que aquí hay algunas reglas sugeridas:

  • Usted puede inline cuando todos los parámetros de tipo funcionales son llamados directa o pasan a otra línea función
  • Usted debe Inline cuando ^ es el caso.
  • No puede en línea cuando el parámetro de función se asigna a una variable dentro de la función
  • Usted debe considerar inlining si al menos uno de los parámetros de tipo funcional puede ser inline, el uso noinlinede los demás.
  • No debe incluir funciones enormes en línea, piense en el código de bytes generado. Se copiará en todos los lugares desde donde se llama a la función.
  • Otro caso de uso son los reifiedparámetros de tipo, que requieren su uso inline. Leer aquí .
s1m0nw1
fuente
4
técnicamente, todavía podría funciones en línea que no toman expresiones lambda, ¿verdad? .. aquí la ventaja es que la sobrecarga de llamadas a la función se evita en ese caso .. lenguaje como Scala permite esto .. no estoy seguro de por qué Kotlin prohíbe ese tipo de en línea- ing
rogue-one
3
@ rogue-one Kotlin no prohíbe este momento de inlining. Los autores del lenguaje simplemente afirman que es probable que el beneficio de rendimiento sea insignificante. Es probable que la JVM ya incorpore métodos pequeños durante la optimización JIT, especialmente si se ejecutan con frecuencia. Otro caso en el que inlinepuede ser perjudicial es cuando el parámetro funcional se llama varias veces en la función en línea, como en diferentes ramas condicionales. Me encontré con un caso en el que todo el código de bytes para argumentos funcionales se estaba duplicando debido a esto.
Mike Hill
5

El caso más importante cuando usamos el modificador en línea es cuando definimos funciones similares a utilidades con funciones de parámetros. Colección o procesamiento de cadenas (como filter, mapo joinToString) o simplemente funciones independientes son un ejemplo perfecto.

Es por eso que el modificador en línea es principalmente una optimización importante para los desarrolladores de bibliotecas. Deben saber cómo funciona y cuáles son sus mejoras y costos. Deberíamos usar el modificador en línea en nuestros proyectos cuando definimos nuestras propias funciones util con parámetros de tipo de función.

Si no tenemos un parámetro de tipo de función, un parámetro de tipo reificado y no necesitamos un retorno no local, lo más probable es que no debamos usar el modificador en línea. Es por eso que tendremos una advertencia en Android Studio o IDEA IntelliJ.

Además, existe un problema de tamaño de código. Insertar una función grande podría aumentar drásticamente el tamaño del código de bytes porque se copia en cada sitio de llamada. En tales casos, puede refactorizar la función y extraer el código a funciones normales.

0xAliHn
fuente
4

Las funciones de orden superior son muy útiles y realmente pueden mejorar el reusabilitycódigo. Sin embargo, una de las mayores preocupaciones sobre su uso es la eficiencia. Las expresiones Lambda se compilan en clases (a menudo clases anónimas) y la creación de objetos en Java es una operación pesada. Todavía podemos usar funciones de orden superior de una manera eficaz, manteniendo todos los beneficios, haciendo funciones en línea.

aquí viene la función en línea en la imagen

Cuando una función está marcada como inline, durante la compilación del código, el compilador reemplazará todas las llamadas a la función con el cuerpo real de la función. Además, las expresiones lambda proporcionadas como argumentos se reemplazan con su cuerpo real. No se tratarán como funciones, sino como código real.

En resumen: - Inline -> en lugar de ser llamados, son reemplazados por el código del cuerpo de la función en tiempo de compilación ...

En Kotlin, usar una función como parámetro de otra función (las llamadas funciones de orden superior) se siente más natural que en Java.

Sin embargo, usar lambdas tiene algunas desventajas. Dado que son clases anónimas (y, por lo tanto, objetos), necesitan memoria (e incluso podrían aumentar el recuento general de métodos de su aplicación). Para evitar esto, podemos integrar nuestros métodos.

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

Del ejemplo anterior : - Estas dos funciones hacen exactamente lo mismo: imprimir el resultado de la función getString. Uno está alineado y el otro no.

Si revisa el código java descompilado, verá que los métodos son completamente idénticos. Eso es porque la palabra clave en línea es una instrucción para el compilador para copiar el código en el sitio de llamada.

Sin embargo, si estamos pasando cualquier tipo de función a otra función como a continuación:

//Compile time error… Illegal usage of inline function type ftOne...
 inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
 }

Para resolver eso, podemos reescribir nuestra función de la siguiente manera:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

Supongamos que tenemos una función de orden superior como la siguiente:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

Aquí, el compilador nos dirá que no usemos la palabra clave en línea cuando solo hay un parámetro lambda y lo estamos pasando a otra función. Entonces, podemos reescribir la función anterior de la siguiente manera:

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
}

Nota : -¡Tuvimos que eliminar la palabra clave noinline también porque solo se puede usar para funciones en línea!

Supongamos que tenemos una función como esta ->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}

Esto funciona bien, pero la esencia de la lógica de la función está contaminada con el código de medición, lo que dificulta que sus colegas trabajen en lo que está sucediendo. :)

Así es como una función en línea puede ayudar a este código:

      fun intercept() {
    // ...
    val result = measure { doSomethingWeWantToMeasure() }
    // ...
    }

     inline fun <T> measure(action: () -> T) {
    val start = SystemClock.elapsedRealtime()
    val result = action()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    return result
    }

Ahora puedo concentrarme en leer cuál es la intención principal de la función intercept () sin saltarme las líneas del código de medición. También nos beneficiamos de la opción de reutilizar ese código en otros lugares donde queramos

inline le permite llamar a una función con un argumento lambda dentro de un cierre ({...}) en lugar de pasar la medida lambda como (myLamda)

Wini
fuente
2

Un caso simple en el que puede querer uno es cuando crea una función util que toma un bloque de suspensión. Considera esto.

fun timer(block: () -> Unit) {
    // stuff
    block()
    //stuff
}

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

    // This is an error
    timer { asyncLogic() }
}

En este caso, nuestro temporizador no aceptará funciones de suspensión. Para resolverlo, es posible que tenga la tentación de suspenderlo también

suspend fun timer(block: suspend () -> Unit) {
    // stuff
    block()
    // stuff
}

Pero entonces solo se puede usar desde coroutines / suspend functions en sí. Luego, terminará haciendo una versión asíncrona y una versión no asíncrona de estas utilidades. El problema desaparece si lo hace en línea.

inline fun timer(block: () -> Unit) {
    // stuff
    block()
    // stuff
}

fun main() {
    // timer can be used from anywhere now
    timer { logic() }

    launch {
        timer { asyncLogic() }
    }
}

Aquí hay un parque infantil de Kotlin. con el estado de error. Haz que el temporizador esté en línea para resolverlo.

Anthony Naddeo
fuente