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 descubrí que no hay un 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(); } }Permítanme agregar: Cuándo no usar inline :
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
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á, dando el error:
Uso ilegal de 'operación' de parámetro en línea en '...'. Agregue el modificador 'noinline' a la declaración del parámetro.
La razón es que el compilador no puede alinear este código, particularmente el parámetro de operation . Si operation no está envuelta en un objeto (que sería el resultado de aplicar inline ), ¿cómo se puede asignar a una variable? En este caso, el compilador sugiere hacer el argumento noinline . Tener una función inline con una noinline función sin línea no tiene ningún sentido, no hagas eso. Sin embargo, si hay varios parámetros de tipos funcionales, considere insertar algunos de ellos si es necesario.
Así que aquí hay algunas reglas sugeridas:
noinline para los demás.reified , que requieren que use inline . Lea aquí .El caso más importante cuando usamos el modificador en línea es cuando definimos funciones de utilidad con funciones de parámetros. La recopilación o el procesamiento de cadenas (como filter , map o joinToString ) o simplemente las funciones independientes son un ejemplo perfecto.
Esta es la razón por la 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 de utilidad con parámetros de tipo de función.
Si no tenemos un parámetro de tipo de función, un parámetro de tipo cosificado 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, hay 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 llamadas. En tales casos, puede refactorizar la función y extraer el código a funciones regulares.
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 lenguaje Java, esto se traducirá a 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 creará aquí una instancia de Function , que envuelve el código dentro de la lambda (nuevamente, esto se simplifica):
nonInlined(new Function() { @Override public void invoke() { System.out.println("do something here"); } }); Básicamente, llamar a esta función y pasarle una lambda siempre creará una instancia de un objeto Function .
Por otro lado, si usa la palabra clave inline :
inline fun inlined(block: () -> Unit) { println("before") block() println("after") }Cuando lo llamas así:
inlined { println("do something here") } No se creará ninguna instancia Function ; en cambio, el código alrededor de la invocación del block dentro 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.
Las funciones de orden superior son muy útiles y realmente pueden mejorar la reusability del có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 manera efectiva, manteniendo todos los beneficios, haciendo funciones en línea.
aquí viene la función en línea en la imagen
Cuando una función se marca como inline , durante la compilación del código, el compilador reemplazará todas las llamadas de 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: - En línea -> 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, el uso de lambdas tiene algunas desventajas. Dado que son clases anónimas (y, por lo tanto, objetos), necesitan memoria (e incluso podrían agregarse al recuento general de métodos de su aplicación). Para evitar esto, podemos alinear 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 verificara el código Java descompilado, vería que los métodos son completamente idénticos. Esto se debe a que la palabra clave en línea es una instrucción para que el compilador copie el código en el sitio de la llamada.
Sin embargo, si estamos pasando cualquier tipo de función a otra función como la siguiente:
//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 : ¡también tuvimos que eliminar la palabra clave noinline 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 líneas de código de medición. También nos beneficiamos de la opción de reutilizar ese código en otros lugares donde queramos
en línea le permite llamar a una función con un argumento lambda dentro de un cierre ({...}) en lugar de pasar la lambda como medida (myLamda)
La palabra clave en línea es útil para funciones que aceptan otras funciones, o lambdas, como argumentos.
Sin la palabra clave en línea en una función, el argumento lambda de esa función se convierte en el momento de la compilación en una instancia de una interfaz de función con un único método llamado invocar(), y el código en la lambda se ejecuta llamando a invocar() en esa instancia de función dentro del cuerpo de la función.
Con la palabra clave en línea en una función, esa conversión de tiempo de compilación nunca ocurre. En cambio, el cuerpo de la función en línea se inserta en su sitio de llamada y su código se ejecuta sin la sobrecarga de crear una instancia de función.
¿Mmm? Ejemplo en android -->
Digamos que tenemos una función en una clase de enrutador de actividad para iniciar una actividad y aplicar algunos extras
fun startActivity(context: Context, activity: Class<*>, applyExtras: (intent: Intent) -> Unit) { val intent = Intent(context, activity) applyExtras(intent) context.startActivity(intent) }Esta función crea una intención, aplica algunos extras llamando al argumento de la función applyExtras e inicia la actividad.
Si miramos el código de bytes compilado y lo descompilamos en Java, esto se parece a:
void startActivity(Context context, Class activity, Function1 applyExtras) { Intent intent = new Intent(context, activity); applyExtras.invoke(intent); context.startActivity(intent); }Digamos que llamamos a esto desde un detector de clics en una actividad:
override fun onClick(v: View) { router.startActivity(this, SomeActivity::class.java) { intent -> intent.putExtra("key1", "value1") intent.putExtra("key2", 5) } }El código de bytes descompilado para este detector de clics se vería así:
@Override void onClick(View v) { router.startActivity(this, SomeActivity.class, new Function1() { @Override void invoke(Intent intent) { intent.putExtra("key1", "value1"); intent.putExtra("key2", 5); } } }Se crea una nueva instancia de Function1 cada vez que se activa el detector de clics. ¡Esto funciona bien, pero no es lo ideal!
Ahora agreguemos en línea a nuestro método de enrutador de actividad:
inline fun startActivity(context: Context, activity: Class<*>, applyExtras: (intent: Intent) -> Unit) { val intent = Intent(context, activity) applyExtras(intent) context.startActivity(intent) }Sin cambiar nuestro código de escucha de clics en absoluto, ahora podemos evitar la creación de esa instancia de Function1. El equivalente de Java del código de escucha de clics ahora se vería así:
@Override void onClick(View v) { Intent intent = new Intent(context, SomeActivity.class); intent.putExtra("key1", "value1"); intent.putExtra("key2", 5); context.startActivity(intent); }Para "en línea" una función básicamente significa copiar el cuerpo de una función y pegarlo en el sitio de llamada de la función. Esto sucede en tiempo de compilación.
Un caso simple en el que podría querer uno es cuando crea una función de utilidad que admite 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 hacer que se suspenda también
suspend fun timer(block: suspend () -> Unit) { // stuff block() // stuff }Pero entonces solo se puede usar desde coroutines/funciones de suspensión 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 haces 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 patio de recreo de kotlin con el estado de error. Haz que el temporizador esté en línea para resolverlo.
inline para evitar la creación de objetosLas lambdas se convierten en clases.
En Kotlin/JVM, los tipos de función (lambdas) se convierten en clases anónimas/normales que amplían la interfaz Function . Considere la siguiente función:
fun doSomethingElse(lambda: () -> Unit) { println("Doing something else") lambda() }La función anterior, después de la compilación, se verá así:
public static final void doSomethingElse(Function0 lambda) { System.out.println("Doing something else"); lambda.invoke(); } El tipo de función () -> Unit se convierte a la interfaz Function0 .
Ahora veamos qué sucede cuando llamamos a esta función desde alguna otra función:
fun doSomething() { println("Before lambda") doSomethingElse { println("Inside lambda") } println("After lambda") }Problema: objetos
El compilador reemplaza la lambda con un objeto anónimo de tipo Function :
public static final void doSomething() { System.out.println("Before lambda"); doSomethingElse(new Function() { public final void invoke() { System.out.println("Inside lambda"); } }); System.out.println("After lambda"); }El problema aquí es que, si llamas a esta función en un bucle miles de veces, se crearán miles de objetos y se recolectarán basura. Esto afecta el rendimiento.
Solución: inline
Al agregar la palabra clave inline antes de la función, podemos decirle al compilador que copie el código de esa función en el sitio de llamada, sin crear los objetos :
inline fun doSomethingElse(lambda: () -> Unit) { println("Doing something else") lambda() } Esto da como resultado la copia del código de la función inline , así como el código de lambda() en el sitio de la llamada:
public static final void doSomething() { System.out.println("Before lambda"); System.out.println("Doing something else"); System.out.println("Inside lambda"); System.out.println("After lambda"); } Esto duplica la velocidad de ejecución, si compara con/sin palabra clave inline con un millón de repeticiones en un bucle for . Entonces, las funciones que toman otras funciones como argumentos son más rápidas cuando están en línea.
inline para evitar la captura de variablesCuando usa las variables locales dentro de la lambda, se llama captura de variable (cierre):
fun doSomething() { val greetings = "Hello" // Local variable doSomethingElse { println("$greetings from lambda") // Variable capture } } Si nuestra función doSomethingElse() aquí no está en inline , las variables capturadas se pasan a la lambda a través del constructor mientras se crea el objeto anónimo que vimos anteriormente:
public static final void doSomething() { String greetings = "Hello"; doSomethingElse(new Function(greetings) { public final void invoke() { System.out.println(this.$greetings + " from lambda"); } }); } Si tiene muchas variables locales usadas dentro de lambda o llamando a lambda en un bucle, pasar cada variable local a través del constructor provoca la sobrecarga de memoria adicional. Usar la función en inline en este caso ayuda mucho, ya que la variable se usa directamente en el sitio de la llamada.
Entonces, como puede ver en los dos ejemplos anteriores, la gran parte del beneficio de rendimiento de las funciones inline se logra cuando las funciones toman otras funciones como argumentos. Aquí es cuando las funciones inline son más beneficiosas y vale la pena usarlas. No hay necesidad de inline otras funciones generales porque el compilador JIT ya las hace en línea bajo el capó, siempre que se considere necesario.
inline para un mejor flujo de control Dado que el tipo de función no en línea se convierte en una clase, no podemos escribir la declaración de return dentro de la lambda:
fun doSomething() { doSomethingElse { return // Error: return is not allowed here } } Esto se conoce como return no local porque no es local para la función de llamada doSomething() . La razón para no permitir la return no local es que la declaración de return existe en otra clase (en la clase anónima que se muestra anteriormente). Hacer que la función doSomethingElse() esté en inline resuelve este problema y se nos permite usar devoluciones no locales porque entonces la declaración de return se copia dentro de la función de llamada.
inline para parámetros de tipo reified Mientras usamos genéricos en Kotlin, podemos trabajar con el valor de tipo T . Pero no podemos trabajar con el tipo directamente, obtenemos el error Cannot use 'T' as reified type parameter. Use a class instead :
fun <T> doSomething(someValue: T) { println("Doing something with value: $someValue") // OK println("Doing something with type: ${T::class.simpleName}") // Error }Esto se debe a que el argumento de tipo que le pasamos a la función se borra en tiempo de ejecución. Por lo tanto, no podemos saber exactamente con qué tipo estamos tratando.
El uso de una función inline junto con el parámetro de tipo reified resuelve este problema:
inline fun <reified T> doSomething(someValue: T) { println("Doing something with value: $someValue") // OK println("Doing something with type: ${T::class.simpleName}") // OK } La inserción hace que el argumento de tipo real se copie en lugar de T . Entonces, por ejemplo, T::class.simpleName se convierte en String::class.simpleName , cuando llamas a la función como doSomething("Some String") . La palabra clave reified solo se puede usar con funciones inline .
inline cuando las llamadas son repetitivasDigamos que tenemos la siguiente función que se llama repetidamente en diferentes niveles de abstracción:
inline fun doSomething() { println("Doing something") }Primer nivel de abstracción
inline fun doSomethingAgain() { doSomething() doSomething() }Resultados en:
public static final void doSomethingAgain() { System.out.println("Doing something"); System.out.println("Doing something"); }En el primer nivel de abstracción, el código crece en: 2 1 = 2 líneas.
Segundo nivel de abstracción
inline fun doSomethingAgainAndAgain() { doSomethingAgain() doSomethingAgain() }Resultados en:
public static final void doSomethingAgainAndAgain() { System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); }En el segundo nivel de abstracción, el código crece en: 2 2 = 4 líneas.
Tercer nivel de abstracción
inline fun doSomethingAgainAndAgainAndAgain() { doSomethingAgainAndAgain() doSomethingAgainAndAgain() }Resultados en:
public static final void doSomethingAgainAndAgainAndAgain() { System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); System.out.println("Doing something"); }En el tercer nivel de abstracción, el código crece en: 2 3 = 8 líneas.
De manera similar, en el cuarto nivel de abstracción, el código crece en 2 4 = 16 líneas y así sucesivamente.
El número 2 es el número de veces que se llama a la función en cada nivel de abstracción. Como puede ver, el código crece exponencialmente no solo en el último nivel sino también en todos los niveles, por lo que son 16 + 8 + 4 + 2 líneas. He mostrado solo 2 llamadas y 3 niveles de abstracción aquí para que sea conciso, pero imagine cuánto código se generará para más llamadas y más niveles de abstracción. Esto aumenta el tamaño de su aplicación. Esta es otra razón por la que no debe inline todas y cada una de las funciones de su aplicación.
inline en ciclos recursivos Evite usar la función en inline para ciclos recursivos de llamadas a funciones como se muestra en el siguiente código:
// Don't use inline for such recursive cycles inline fun doFirstThing() { doSecondThing() } inline fun doSecondThing() { doThirdThing() } inline fun doThirdThing() { doFirstThing() } Esto dará como resultado un ciclo interminable de funciones que copian el código. El compilador le da un error: The 'yourFunction()' invocation is a part of inline cycle .
inline al ocultar la implementación Las funciones públicas inline no pueden acceder a las funciones private , por lo que no se pueden usar para ocultar la implementación:
inline fun doSomething() { doItPrivately() // Error } private fun doItPrivately() { } En la función inline que se muestra arriba, acceder a la función private doItPrivately() un error: Public-API inline function cannot access non-public API fun .
Ahora, sobre la segunda parte de tu pregunta:
pero descubrí que no hay un objeto de función creado por kotlin para una función no en línea. ¿Por qué?
De hecho, se crea el objeto Function . Para ver el objeto Function creado, debe llamar a su función lock() dentro de la función main() de la siguiente manera:
fun main() { lock { println("Inside the block()") } }clase generada
La clase Function generada no se refleja en el código Java descompilado. Debe mirar directamente en el código de bytes. Busque la línea que comienza con:
final class your/package/YourFilenameKt$main$1 extends Lambda implements Function0 { } Esta es la clase que genera el compilador para el tipo de función que se pasa a la función lock() . El main$1 es el nombre de la clase que se crea para su función block() . A veces la clase es anónima como se muestra en el ejemplo de la primera sección.
objeto generado
En el código de bytes, busque la línea que comienza con:
GETSTATIC your/package/YourFilenameKt$main$1.INSTANCE INSTANCE es el objeto que se crea para la clase mencionada anteriormente. El objeto creado es un singleton, de ahí el nombre INSTANCE .
¡Eso es todo! Espero que proporcione información útil sobre las funciones en inline .
fun higherOrder(lambda:():Unit){ //invoking lambda lambda() } //Normal function calling higher-order without inline fun callingHigerOrder() { higherOrder() //Here an object will be created for the lambda inside the higher-order function } //Normal function calling higher-order with inline fun callingHigerOrder() { higherOrder() //Here there will be no object created and the contents of the lambda will be called directly into this calling function. }use en línea si desea evitar la creación de objetos en el lado de la llamada. Entonces, cuando se usa en línea, como entendimos, lambda será parte de la función de llamada en caso de que haya una llamada de devolución dentro del bloque lambda, entonces se devolverá la función de llamada completa, esto se llama devolución no local. Para evitar el retorno no local, use cross-inline antes del bloque lambda en la función de orden superior.