web-dev-qa-db-ja.com

Kotlin:安全なラムダ(メモリリークなし)?

メモリリークに関するこの記事 を読んだ後、Kotlinでラムダを使用することは安全かどうか疑問に思っていますAndroidプロジェクト。ラムダ構文によりプログラムがより簡単になります。しかし、メモリリークはどうですか?

問題のある例として、私はAlertDialogを構築するプロジェクトの1つからコードを取得しました。このコードは、私のプロジェクトのMainActivityクラス内にあります。

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()
}

私の質問は非常に簡単です:正と負のボタンに設定された2つのラムダはメモリリークにつながる可能性がありますか? (つまり、kotlin lambdaは単にJava無名関数?)

編集:たぶん私は私の答えを持っている このJetbrainsトピックで

28
loloof64

編集(2017年2月19日):この問題に関して、Mike Hearnから非常に包括的な reply を受け取りました。

Javaのように、Kotlinで起こることはさまざまなケースで異なります。

  • ラムダがインライン関数に渡され、noinlineとマークされていない場合、全体が沸騰し、追加のクラスやオブジェクトは作成されません。
  • ラムダがキャプチャしない場合、インスタンスが何度も再利用されるシングルトンクラスとして発行されます(1つのクラス+ 1つのオブジェクト割り当て)。
  • ラムダがキャプチャすると、ラムダが使用されるたびに新しいオブジェクトが作成されます。

したがって、Javaと似た振る舞いです。ただし、インライン化の場合はより安価です。ラムダをエンコードするこの効率的なアプローチは、Kotlinの関数型プログラミングがJavaよりも魅力的である理由の1つです。


編集(2017年2月17日):Kotlinディスカッション でこのトピックに関する質問を投稿しました。 Kotlinのエンジニアがテーブルに何か新しいものをもたらすかもしれません。


kotlin lambdaは単にJava無名関数?

私は自分でこの質問をしていました(ここで1つの簡単な修正:これらは匿名クラスと呼ばれ、関数ではありません)。 Koltinドキュメントには明確な答えはありません。彼らはただ state that

高階関数を使用すると、特定のランタイムペナルティが課せられます。各関数はオブジェクトであり、クロージャー、つまり関数の本体でアクセスされる変数をキャプチャします。

関数の本体でアクセスされる変数の意味が少しわかりにくいです。囲んでいるクラスのインスタンスへの参照もカウントされますか?

あなたの質問であなたが参照しているトピックを見てきましたが、今のところは時代遅れのようです。最新の情報が見つかりました こちら

外側のクラスのラムダ式または匿名関数暗黙的な参照を保持する

そのため、残念ながら、KotlinのラムダにはJavaの匿名内部クラスと同じ問題があるようです。

匿名の内部クラスが悪いのはなぜですか?

Javaから specs

クラスOの直接内部クラスCのインスタンスiは、iのすぐに囲まれたインスタンスとして知られるOのインスタンスに関連付けられます。オブジェクトの即時囲みインスタンス(ある場合)は、オブジェクトの作成時に決定されます

これが意味することは、匿名クラスは常に、囲むクラスのインスタンスへのimplicit参照を持っているということです。また、参照は暗黙的であるため、参照を削除する方法はありません。

些細な例を見てください

_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();
   }
}
_

ご覧のとおり、この場合、長時間実行されるタスクが実行されるまでメモリリークが発生します。これに対する1つの回避策は、静的なネストされたクラスを使用することです。

_Kotlin's_ non-inlinedラムダは、囲むクラスのインスタンスへの参照を保持するため、メモリリークに関して同様の問題があります。

ボーナス:他のLambda実装との簡単な比較

Java 8ラムダ

構文:

  • SAM(単一抽象メソッド)インターフェースの宣言

    _interface Runnable { void run(); }
    _
  • このインターフェイスをラムダの型として使用します

    _public void canTakeLambda(Runnable r) { ... }
    _
  • ラムダを渡す

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

メモリリークの問題:specs で述べたとおり:

これへの参照(非修飾フィールド参照またはメソッド呼び出しによる暗黙的な参照を含む)は、本質的には最終的なローカル変数への参照です。このような参照を含むラムダボディは、この適切なインスタンスをキャプチャします。 他の場合、これへの参照は保持されませんオブジェクトによって。

簡単に言うと、外側のクラスのフィールド/メソッドを使用しない場合、匿名クラスの場合のようにthisへの暗黙的な参照はありません。

レトロラムダ

docs から

ラムダ式は、匿名の内部クラスに変換することによりバックポートされます。これには、ステートレスラムダのシングルトンインスタンス式を使用してオブジェクトの割り当てが繰り返されるのを防ぐ最適化が含まれます。

それは一目瞭然だと思います。

アップルのスウィフト

構文:

  • 宣言はKotlinに似ており、Swiftラムダはクロージャと呼ばれます:

    _func someFunctionThatTakesAClosure(closure: (String) -> Void) {}
    _
  • 閉鎖に合格

    _someFunctionThatTakesAClosure { print($0) }
    _

    ここで、_$0_はクロージャーの最初のString引数を参照します。これは、Kotlinのitに対応します。注:Kotlinとは異なり、Swiftでは、_$1_、_$2_などの他の引数も参照できます。

メモリリークの問題:

Swiftでは、in Java 8と同様に、クロージャはselfthis in JavaとKotlin)_self.someProperty_などのインスタンスのプロパティにアクセスする場合、またはクロージャーがself.someMethod()などのインスタンスのメソッドを呼び出す場合のみ。

また、開発者は、弱参照のみをキャプチャすることを簡単に指定できます。

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

Kotlinでも可能だったらいいのにね:)

26
Stan Mots

寿命が長いものがこのオブジェクトへの参照を持っているため、不要になったために削除する必要があるオブジェクトを削除できない場合、メモリリークが発生します。最も単純な例は、Activityへの参照をstatic変数に格納することです(Javaパースペクティブから話していますが、Kotlinでも同様です):ユーザーが[戻る]ボタンをクリックした場合、Activityは不要になりますが、それでもメモリ内に保持されます。これは、一部の静的変数がまだこのアクティビティを指しているためです。
今、あなたの例では、あなたのActivityをいくつかのstatic変数に割り当てていないので、objectを保持できるKotlinのActivitysはありませんガベージコレクションされる-コードに含まれるすべてのオブジェクトのライフタイムはほぼ同じであるため、メモリリークは発生しません。

追伸Kotlinのラムダの実装に関する記憶を更新しました:ネガティブボタンクリックハンドラーの場合、外側のスコープを参照していないため、コンパイラーはクリックリスナーの単一インスタンスを作成し、クリックリスナー全体で再利用しますこのボタンに。ポジティブボタンクリックリスナーの場合、外側のスコープ(this@MainActivity)、この場合、Kotlinはダイアログを作成するたびに匿名クラスの新しいインスタンスを作成します(そしてこのインスタンスは外部クラスMainActivityへの参照を持っているため)、動作は正確ですこのコードをJavaで記述した場合と同じです。

10
aga