quand utiliser une fonction en ligne dans Kotlin?

105

Je sais qu'une fonction en ligne améliorera peut-être les performances et fera croître le code généré, mais je ne sais pas quand il est correct d'en utiliser une.

lock(l) { foo() }

Au lieu de créer un objet fonction pour le paramètre et de générer un appel, le compilateur peut émettre le code suivant. ( Source )

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

mais j'ai trouvé qu'il n'y a pas d'objet fonction créé par kotlin pour une fonction non en ligne. Pourquoi?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
holi-java
la source
7
Il existe deux principaux cas d'utilisation pour cela, l'un avec certains types de fonctions d'ordre supérieur et l'autre avec des paramètres de type réifié. La documentation des fonctions en ligne couvre celles-ci: kotlinlang.org/docs/reference/inline-functions.html
zsmb13
2
@ zsmb13 merci, monsieur. mais je ne comprends pas que: "Au lieu de créer un objet fonction pour le paramètre et de générer un appel, le compilateur pourrait émettre le code suivant"
holi-java
2
je n'obtiens pas cet exemple non plus tbh.
filthy_wizard

Réponses:

280

Disons que vous créez une fonction d'ordre supérieur qui prend un lambda de type () -> Unit(pas de paramètres, pas de valeur de retour), et l'exécute comme ceci:

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

En langage Java, cela se traduira par quelque chose comme ceci (simplifié!):

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

Et quand vous l'appelez de Kotlin ...

nonInlined {
    println("do something here")
}

Sous le capot, une instance de Functionsera créée ici, qui encapsule le code à l'intérieur du lambda (encore une fois, cela est simplifié):

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

Donc, fondamentalement, appeler cette fonction et lui passer un lambda créera toujours une instance d'un Functionobjet.


En revanche, si vous utilisez le inlinemot - clé:

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

Quand vous l'appelez comme ceci:

inlined {
    println("do something here")
}

Aucune Functioninstance ne sera créée, à la place, le code autour de l'invocation de l' blockintérieur de la fonction en ligne sera copié sur le site d'appel, vous obtiendrez donc quelque chose comme ça dans le bytecode:

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

Dans ce cas, aucune nouvelle instance n'est créée.

zsmb13
la source
19
Quel est l'avantage d'avoir le wrapper d'objet Function en premier lieu? c'est-à-dire - pourquoi tout n'est-il pas en ligne?
Arturs Vancans
14
De cette façon, vous pouvez également passer arbitrairement des fonctions en tant que paramètres, les stocker dans des variables, etc.
zsmb13
6
Grande explication par @ zsmb13
Yajairo87
2
Vous pouvez, et si vous faites des choses compliquées avec eux, vous voudrez éventuellement connaître les mots-clés noinlineet crossinline- consultez la documentation .
zsmb13
2
Les documents donnent une raison pour laquelle vous ne voudriez pas insérer
CorayJeu
43

Permettez-moi d'ajouter: "Quand ne pas utiliser inline" :

1) Si vous avez une fonction simple qui n'accepte pas d'autres fonctions comme argument, cela n'a pas de sens de les insérer. IntelliJ vous avertira:

L'impact attendu de l'inlining «...» sur les performances est insignifiant. L'intégration fonctionne mieux pour les fonctions avec des paramètres de types fonctionnels

2) Même si vous avez une fonction "avec des paramètres de types fonctionnels", vous pouvez rencontrer le compilateur vous disant que l'inlining ne fonctionne pas. Prenons cet exemple:

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

Ce code ne se compilera pas avec l'erreur:

Utilisation illégale du paramètre en ligne «opération» dans «...». Ajoutez le modificateur 'noinline' à la déclaration de paramètre.

La raison en est que le compilateur ne parvient pas à insérer ce code. Si operationn'est pas enveloppé dans un objet (ce qui est impliqué par inlinepuisque vous voulez éviter cela), comment peut-il être affecté à une variable? Dans ce cas, le compilateur suggère de faire l'argument noinline. Avoir une inlinefonction avec une seule noinlinefonction n'a aucun sens, ne faites pas ça. Cependant, s'il existe plusieurs paramètres de types fonctionnels, envisagez d'en intégrer certains si nécessaire.

Voici donc quelques règles suggérées:

  • Vous pouvez en ligne lorsque tous les paramètres de type fonctionnel sont appelés directement ou passés à une autre fonction en ligne
  • Vous devriez en ligne quand ^ est le cas.
  • Vous ne pouvez pas en ligne lorsque le paramètre de fonction est affecté à une variable à l'intérieur de la fonction
  • Vous devriez envisager l'inlining si au moins un de vos paramètres de type fonctionnel peut être inséré, utilisez noinlinepour les autres.
  • Vous ne devriez pas intégrer de fonctions énormes, pensez au code d'octet généré. Il sera copié à tous les endroits d'où la fonction est appelée.
  • Un autre cas d'utilisation concerne reifiedles paramètres de type, qui nécessitent que vous les utilisiez inline. Lisez ici .
s1m0nw1
la source
4
techniquement, vous pourriez toujours des fonctions en ligne qui ne prennent pas les expressions lambda, n'est-ce pas? ... ici, l'avantage est que la surcharge d'appel de fonction est évitée dans ce cas ... un langage comme Scala le permet ... je ne sais pas pourquoi Kotlin interdit ce type de connexion en ligne. ing
rogue-one
3
@ rogue-one Kotlin n'interdit pas cette fois la mise en ligne. Les auteurs du langage prétendent simplement que l'avantage de performance est probablement insignifiant. Les petites méthodes sont déjà susceptibles d'être intégrées par la JVM lors de l'optimisation JIT, surtout si elles sont exécutées fréquemment. Un autre cas où inlinepeut être nuisible est lorsque le paramètre fonctionnel est appelé plusieurs fois dans la fonction en ligne, comme dans différentes branches conditionnelles. Je viens de tomber sur un cas où tout le bytecode des arguments fonctionnels était dupliqué à cause de cela.
Mike Hill le
5

Le cas le plus important lorsque nous utilisons le modificateur en ligne est lorsque nous définissons des fonctions de type util avec des fonctions de paramètres. Le traitement de collection ou de chaîne (comme filter, mapou joinToString) ou simplement des fonctions autonomes en sont un exemple parfait.

C'est pourquoi le modificateur en ligne est principalement une optimisation importante pour les développeurs de bibliothèques. Ils doivent savoir comment cela fonctionne, quelles sont ses améliorations et ses coûts. Nous devrions utiliser le modificateur en ligne dans nos projets lorsque nous définissons nos propres fonctions util avec des paramètres de type de fonction.

Si nous n'avons pas de paramètre de type de fonction, de paramètre de type réifié et que nous n'avons pas besoin de retour non local, nous ne devrions probablement pas utiliser le modificateur en ligne. C'est pourquoi nous aurons un avertissement sur Android Studio ou IDEA IntelliJ.

Il existe également un problème de taille de code. L'intégration d'une fonction volumineuse peut augmenter considérablement la taille du bytecode car il est copié sur chaque site d'appel. Dans de tels cas, vous pouvez refactoriser la fonction et extraire le code en fonctions régulières.

0xAliHn
la source
4

Les fonctions d'ordre supérieur sont très utiles et peuvent vraiment améliorer le reusabilitycode. Cependant, l'une des plus grandes préoccupations liées à leur utilisation est l'efficacité. Les expressions Lambda sont compilées en classes (souvent des classes anonymes) et la création d'objets en Java est une opération lourde. Nous pouvons toujours utiliser des fonctions d'ordre supérieur de manière efficace, tout en conservant tous les avantages, en rendant les fonctions en ligne.

voici la fonction en ligne en image

Lorsqu'une fonction est marquée comme inline, lors de la compilation du code, le compilateur remplacera tous les appels de fonction par le corps réel de la fonction. De plus, les expressions lambda fournies en tant qu'arguments sont remplacées par leur corps réel. Ils ne seront pas traités comme des fonctions, mais comme du code réel.

En bref: - Inline -> plutôt que d'être appelés, ils sont remplacés par le code du corps de la fonction au moment de la compilation ...

Dans Kotlin, utiliser une fonction comme paramètre d'une autre fonction (appelées fonctions d'ordre supérieur) semble plus naturel qu'en Java.

Cependant, l'utilisation de lambdas présente certains inconvénients. Puisqu'il s'agit de classes anonymes (et donc d'objets), elles ont besoin de mémoire (et peuvent même ajouter au nombre global de méthodes de votre application). Pour éviter cela, nous pouvons intégrer nos méthodes.

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

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

De l'exemple ci-dessus : - Ces deux fonctions font exactement la même chose - imprimer le résultat de la fonction getString. L'un est en ligne et l'autre ne l'est pas.

Si vous vérifiez le code java décompilé, vous verrez que les méthodes sont complètement identiques. C'est parce que le mot clé en ligne est une instruction au compilateur pour copier le code dans le site d'appel.

Cependant, si nous passons un type de fonction à une autre fonction comme ci-dessous:

//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)
    /*...*/
 }

Pour résoudre cela, nous pouvons réécrire notre fonction comme ci-dessous:

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)
    /*...*/}

Supposons que nous ayons une fonction d'ordre supérieur comme ci-dessous:

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

Ici, le compilateur nous dira de ne pas utiliser le mot clé inline lorsqu'il n'y a qu'un seul paramètre lambda et que nous le passons à une autre fonction. Ainsi, nous pouvons réécrire la fonction ci-dessus comme ci-dessous:

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

Remarque : -nous avons également dû supprimer le mot-clé noinline car il ne peut être utilisé que pour les fonctions en ligne!

Supposons que nous ayons une fonction comme celle-ci ->

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

Cela fonctionne bien, mais le cœur de la logique de la fonction est pollué par le code de mesure, ce qui rend plus difficile pour vos collègues de travailler sur ce qui se passe. :)

Voici comment une fonction en ligne peut aider ce code:

      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
    }

Maintenant, je peux me concentrer sur la lecture de l'intention principale de la fonction intercept () sans sauter des lignes de code de mesure. Nous bénéficions également de la possibilité de réutiliser ce code dans d'autres endroits où nous voulons

inline vous permet d'appeler une fonction avec un argument lambda dans une fermeture ({...}) plutôt que de passer la mesure lambda like (myLamda)

Wini
la source
2

Un cas simple où vous pourriez en vouloir un est lorsque vous créez une fonction util qui prend dans un bloc de suspension. Considère ceci.

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

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

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

Dans ce cas, notre minuterie n'acceptera pas les fonctions de suspension. Pour le résoudre, vous pourriez être tenté de le faire suspendre également

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

Mais alors, il ne peut être utilisé qu'à partir des coroutines / fonctions de suspension elles-mêmes. Ensuite, vous finirez par créer une version asynchrone et une version non asynchrone de ces utilitaires. Le problème disparaît si vous le faites en ligne.

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

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

    launch {
        timer { asyncLogic() }
    }
}

Voici un terrain de jeu kotlin avec l'état d'erreur. Rendez la minuterie en ligne pour le résoudre.

Anthony Naddeo
la source