web-dev-qa-db-fra.com

Kotlin: des lambdas sûrs (pas de fuite de mémoire)?

Après avoir lu cet article sur les fuites de mémoire , je me demande si l'utilisation de lambdas dans Kotlin Android est sûre. Il est vrai que la syntaxe lambda me permet de programmer plus facilement, mais qu'en est-il des fuites de mémoire?

Comme exemple de la problématique, j'ai pris un morceau de code d'un de mes projets, où je construis un AlertDialog. Ce code est dans la classe MainActivity de mon projet.

fun deleteItemOnConfirmation(id: Long) : Unit {
        val item = explorerAdapter.getItemAt(id.toInt())
        val stringId = if (item.isDirectory) R.string.about_to_delete_folder else R.string.about_to_delete_file

        val dialog = AlertDialog.Builder(this).
                setMessage(String.format(getString(stringId), item.name)).setPositiveButton(
                R.string.ok, {dialog: DialogInterface, id: Int ->
                        val success = if (item.isDirectory) ExplorerFileManager.deleteFolderRecursively(item.name)
                        else ExplorerFileManager.deleteFile(item.name)
                        if (success) {
                            explorerAdapter.deleteItem(item)
                            explorerRecyclerView.invalidate()
                        }
                        else Toast.makeText(this@MainActivity, R.string.file_deletion_error, Toast.LENGTH_SHORT).show()
                    }).setNegativeButton(
                R.string.cancel, {dialog: DialogInterface, id: Int ->
                    dialog.cancel()
        })

        dialog.show()
}

Ma question est très simple: les deux lambdas définis pour les boutons positifs et négatifs peuvent-ils entraîner des fuites de mémoire? (Je veux dire aussi, les lambdas kotlin sont-ils simplement convertis en Java Fonctions anonymes?)

Edit: Peut-être que j'ai ma réponse dans cette rubrique Jetbrains .

28
loloof64

Edit (19 février 2017): J'ai reçu une réponse très complète réponse de Mike Hearn concernant ce problème:

Comme en Java, ce qui se passe dans Kotlin varie dans différents cas.

  • Si le lambda est passé à une fonction inline et n'est pas marqué noinline, alors tout se résume et aucune classe ou objet supplémentaire n'est créé.
  • Si le lambda ne capture pas, il sera émis en tant que classe singleton dont l'instance est réutilisée encore et encore (une classe + une allocation d'objet).
  • Si le lambda capture, un nouvel objet est créé chaque fois que le lambda est utilisé.

C'est donc un comportement similaire à Java sauf dans le cas de l'inline où il est encore moins cher. Cette approche efficace pour encoder les lambdas est l'une des raisons pour lesquelles la programmation fonctionnelle dans Kotlin est plus attrayante que dans Java.


Edit (17 février 2017): J'ai posté une question concernant ce sujet dans les discussions Kotlin . Peut-être que les ingénieurs de Kotlin apporteront quelque chose de nouveau à la table.


les lambdas kotlin sont-ils simplement convertis en Java Fonctions anonymes?

Je posais cette question moi-même (une simple correction ici: on les appelle Classes anonymes, pas des fonctions). Il n'y a pas de réponse claire dans la documentation Koltin. Ils ont juste état que

L'utilisation de fonctions d'ordre supérieur impose certaines pénalités d'exécution: chaque fonction est un objet, et elle capture une fermeture, c'est-à-dire les variables qui sont accessibles dans le corps de la fonction.

C'est un peu déroutant ce qu'ils entendent par variables accessibles dans le corps de la fonction. La référence à l'instance de la classe englobante est-elle également prise en compte?

J'ai vu le sujet auquel vous faites référence dans votre question, mais il semble qu'il soit dépassé pour l'instant. J'ai trouvé des informations plus à jour ici :

L'expression lambda ou la fonction anonyme conserve une référence implicite de la classe englobante

Donc, malheureusement, il semble que les lambdas de Kotlin ont les mêmes problèmes que les classes internes anonymes de Java.

Pourquoi les classes internes anonymes sont mauvaises?

À partir de Javaspécifications :

Une instance i d'une classe interne directe C d'une classe O est associée à une instance de O, connue comme l'instance immédiatement englobante de i. L'instance immédiatement englobante d'un objet, le cas échéant, est déterminée lors de la création de l'objet

Cela signifie que la classe anonyme aura toujours une référence implicite à l'instance de la classe englobante. Et puisque la référence est implicite, il n'y a aucun moyen de s'en débarrasser.

Regardez l'exemple trivial

public class YourActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Thread(new Runnable() {
                 // the inner class will keep the implicit reference to the outer activity
                @Override
                public void run() {
                 // long-running task
                }
        }).start();
   }
}

Comme vous pouvez le voir, dans ce cas, il y aura une fuite de mémoire jusqu'à l'exécution de la tâche de longue durée. Une solution de contournement consiste à utiliser une classe imbriquée statique.

Étant donné que Kotlin's les lambdas non alignés contiennent la référence à l'instance de la classe englobante, ils ont des problèmes similaires concernant les fuites de mémoire.

Bonus: comparaison rapide avec d'autres implémentations Lambda

Lambdas Java 8

Syntaxe:

  • Déclarez l'interface SAM (méthode abstraite unique)

    interface Runnable { void run(); }
    
  • Utilisez cette interface comme type pour un lambda

    public void canTakeLambda(Runnable r) { ... }
    
  • Passez votre lambda

    canTakeLambda(() -> System.out.println("Do work in lambda..."));
    

Problèmes de fuite de mémoire: Comme indiqué dans spécifications :

Les références à cela - y compris les références implicites via des références de champ non qualifiées ou des invocations de méthode - sont, essentiellement, des références à une variable locale finale. Les corps lambda qui contiennent de telles références capturent l'instance appropriée de cela. Dans d'autres cas, aucune référence à cela n'est conservée par l'objet.

Autrement dit, si vous n'utilisez aucun champ/méthode de la classe englobante, il n'y a pas de référence implicite à this comme dans le cas des classes anonymes.

Retrolambda

Depuis docs

Les expressions lambda sont rétroportées en les convertissant en classes internes anonymes. Cela inclut l'optimisation de l'utilisation d'une instance singleton pour les expressions lambda sans état afin d'éviter l'allocation répétée d'objets.

Je suppose que ça va de soi.

Apple's Swift

Syntaxe:

  • La déclaration est similaire à Kotlin, dans Swift lambdas sont appelés fermetures:

    func someFunctionThatTakesAClosure(closure: (String) -> Void) {}
    
  • Passer la fermeture

    someFunctionThatTakesAClosure { print($0) }
    

    Ici, $0 Fait référence au premier argument String de la fermeture. Cela correspond à it dans Kotlin. Remarque: Contrairement à Kotlin, dans Swift nous pouvons également nous référer aux autres arguments comme $1, $2 Etc.

Problèmes de fuite de mémoire:

Dans Swift, tout comme dans Java 8, la fermeture capture une référence forte à self (this in Java et Kotlin) uniquement s'il accède à une propriété de l'instance, telle que self.someProperty, Ou si la fermeture appelle une méthode sur l'instance, telle que self.someMethod().

Les développeurs peuvent également facilement spécifier qu'ils veulent capturer uniquement la référence faible:

   someFunctionThatTakesAClosure { [weak self] in print($0) }

Je souhaite que ce soit possible à Kotlin aussi :)

26
Stan Mots

Des fuites de mémoire se produisent lorsqu'un objet qui doit être supprimé parce qu'il n'est plus nécessaire ne peut pas être supprimé car quelque chose qui a une durée de vie plus longue a une référence à cet objet. L'exemple le plus simple est de stocker la référence à la Activity dans la variable static (je parle du point de vue Java, mais c'est similaire dans Kotlin): après que l'utilisateur ait cliqué sur le bouton "Retour", le Activity n'est plus nécessaire, mais il sera néanmoins conservé en mémoire - car une variable statique pointe toujours vers cette activité.
Maintenant, dans votre exemple, vous n'affectez pas votre Activity à une variable static, il n'y a pas de objects de Kotlin impliqués qui pourraient garder votre Activity d'être récupéré - tous les objets impliqués dans votre code ont à peu près la même durée de vie, ce qui signifie qu'il n'y aura pas de fuites de mémoire.

P.S. J'ai rafraîchi mes souvenirs sur l'implémentation des lambdas par Kotlin: dans le cas du gestionnaire de clic de bouton négatif, vous ne faites pas référence à la portée externe, donc le compilateur créera une seule instance de l'écouteur de clic qui sera réutilisée à travers tous les clics sur ce bouton. Dans le cas de l'écouteur de clic positif, vous faites référence à la portée externe (this@MainActivity), dans ce cas, Kotlin créera une nouvelle instance de la classe anonyme chaque fois que vous créerez une boîte de dialogue (et cette instance aura la référence à la classe externe, MainActivity), donc le comportement est exactement comme si vous aviez écrit ce code en Java.

10
aga