背景:
クロージャは、関数型言語から派生した概念であり、コードのブロックを値として渡すことを可能にします。Swiftでは、クロージャ(他の言語のラムダに相当)は最初から導入されました。これにより、非同期呼び出しを優雅に実装したり、アクションを委譲したり、コレクションをソートしたり、フィルタリングしたりすることができます。
問題点:
クロージャは周囲のコンテキストから変数をキャプチャします。これらの変数の中にクラスのオブジェクトへの参照がある場合、特にそのクロージャがそのクラスのプロパティとして保存されている場合、リテインサイクルが発生する可能性があります。また、エスケープクロージャとノンエスケープクロージャの違いを理解し、値のキャプチャがどのように機能するかを認識することが重要です。
解決策:
Swiftはクロージャを独立したオブジェクトとして実装しており、コンテキストをキャプチャできますが、クロージャのオーナーはアーキテクチャに注意する必要があります。
コード例:
class Performer { var onComplete: (() -> Void)? func doWork() { onComplete = { print("仕事が完了しました!") } } } // クロージャ内でselfをキャプチャ class Test { var value = 0 func start() { DispatchQueue.global().async { [weak self] in self?.value = 1 } } }
主な特徴:
クロージャがselfを明示的にキャプチャしない場合、リテインサイクルは発生しますか?
はい、クロージャがクラスの強いプロパティとして保存され、内部でselfにアクセスするとリテインサイクルが発生します。selfを明示的に書かなくても、[weak self]や[unowned self]を使用するか、可能な場合はクロージャをローカルにしてください。
エスケープクロージャとノンエスケープクロージャの違いは何ですか?
エスケープクロージャは関数の終了後に呼び出される可能性があり、通常は非同期処理に使用されます。ノンエスケープは、関数の実行が終了する前に実行されます。エスケープでは、selfのキャプチャ順序が異なります。
コード例:
func asyncWork(completion: @escaping () -> Void) { DispatchQueue.global().async { completion() } }
クロージャはキャプチャした変数の値を変更できますか?
はい、クロージャがvarとして宣言された変数をキャプチャした場合、その値を変更できます(値型の場合)。クラスの場合は参照であり、プロパティを常に変更できます。
コード例:
var value = 1 let closure = { value += 1 } closure() print(value) // 2
ViewControllerがクロージャをプロパティとして保存し(例えば、completionHandler)、その内部でselfに直接アクセスします。その結果、リテインサイクルが発生します:ViewController => クロージャ => ViewController。画面をオフにしてもメモリが解放されません。
長所: コードは簡潔で短く見えます。
短所: メモリリークが発生し、ViewControllerがメモリに「滞留」し、潜在的なバグが発生します。
クロージャ内で[weak self]または[unowned self]を使用するか、クロージャがオブジェクトのライフサイクルより長く保存されないようにします。このような場所のレビューはコードレビューで行います。
長所: リソースの適切な解放、予期しないリークがないこと。
短所: [weak self]は、デリファレンス時に注意が必要であり、誤った使用によって暗黙のクラッシュが発生する可能性があります。