Swift: Swiftクロージャ応用:エスケーピングクロージャ、オートクロージャと循環参照
クロージャは非同期プログラミングの中心的な役割を果たしますが、ライフサイクル管理とメモリリークの課題ももたらします。このレッスンではクロージャの高度な使い方を深く掘り下げ、複雑なアプリケーションシナリオで安全かつ効率的に使用できるようにします。
1. 学習目標
@escapingクロージャの使用シナリオとライフサイクル@autoclosureによる遅延評価- クロージャのキャプチャリストと循環参照の解決方法
weakとunownedによるメモリリークの回避- クロージャが値をキャプチャするタイミングの理解
2. モバイル開発者の実話
(1) 課題:ネットワークコールバックのクラッシュとメモリリーク
Charlieはユーザーがコメントを投稿するソーシャルアプリを構築しており、API呼び出し後にUIを更新する必要があります:
func postComment(text: String, onComplete: () -> Void) {
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Compiler error! Non-escaping closure cannot be used in async callback
onComplete()
}
}
コンパイラがエラーを報告します — クロージャは postComment が返った後に実行されますが、onComplete はデフォルトで非エスケーピングです。さらに悪いことに、エスケーピングクロージャを使用した後もメモリリークが発生します:ビューコントローラが破棄されても、クロージャがまだ参照を保持しており、リークが発生します。
(2) 解決策:@escaping + キャプチャリスト
// Mark as escaping closure to allow async execution
func postComment(text: String, onComplete: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self = self else { return }
self.showSuccess()
onComplete()
}
}
3つの変更:@escaping を追加してエスケーピングとしてマーク、[weak self] で弱参照を追加、guard let で安全にアンラップ。
(3) 利点:安全なメモリ管理
| 観点 | Before | After |
|---|---|---|
| 非同期コールバック | コンパイラエラー | 正常に実行 |
| メモリリーク | ViewControllerが解放できない | weak selfが自動的に参照を解除 |
| クラッシュリスク | コールバック時にオブジェクトが既に解放済み | guard letが検出して安全に復帰 |
| コードの意図 | クロージャのライフサイクルが暗黙的 | @escapingがエスケープの意味を明示 |
3. エスケーピングクロージャ(@escaping)
デフォルトでは、クロージャパラメータは非エスケーピングです — 関数が返る前に実行されます。クロージャに @escaping をマークすると、関数のスコープを脱出できるようになります。
sequenceDiagram
participant Caller as 呼び出し元
participant Func as 関数
participant Closure as クロージャ
Caller->>Func: クロージャを渡す
Note over Func: 非エスケーピング: 関数内で実行
Caller->>Func: @escaping クロージャを渡す
Func->>Closure: 外部変数に保存
Func-->>Caller: 関数が返る
Note over Closure: 関数が返った後も呼び出し可能
Caller->>Closure: 非同期コールバックで実行
| 特徴 | 非エスケーピング | @escaping |
|---|---|---|
| 実行タイミング | 関数が返る前 | 関数が返る前または後 |
| 外部保存 | 不可 | 変数/プロパティに保存可能 |
self. の暗黙的参照 |
省略可能 | 明示必須 |
| コンパイラ最適化 | メモリ管理オーバーヘッドなし | ARC管理が必要 |
| パフォーマンス | より良い | わずかな追加オーバーヘッド |
▶ サンプル: エスケーピングクロージャの使用
// ============================================
// Escaping vs non-escaping closures comparison
// ============================================
import Foundation
var completionHandlers: [() -> Void] = []
// Non-escaping closure — synchronous execution inside function
func syncOperation(task: () -> Void) {
print("Starting sync task")
task()
print("Sync task finished")
}
syncOperation {
print(" Running...")
}
// Escaping closure — stored in external array
func asyncOperation(task: @escaping () -> Void) {
print("Adding async task")
completionHandlers.append(task) // Would error without @escaping
}
asyncOperation {
print(" Async task executed")
}
print("Function has returned, closure not yet executed")
// Execute stored closure later
completionHandlers.first?()
出力:
TEXT 📖 参照専用Starting sync task Running... Sync task finished Adding async task Function has returned, closure not yet executed Async task executed
4. オートクロージャ(@autoclosure)
@autoclosure は式を自動的にクロージャでラップし、遅延評価を可能にします:
graph TB
A["assert(condition: 2 > 1)"] --> B["通常評価: 即座に計算"]
C["assert(condition: 2 > 1, message: \"error\")"] --> D["@autoclosure: 条件が偽の場合のみ評価"]
D --> E["文字列連結のオーバーヘッドを回避"]
| シナリオ | 通常パラメータ | @autoclosure |
|---|---|---|
| 評価タイミング | 呼び出し箇所で即座に評価 | クロージャが呼ばれた時のみ評価 |
| パフォーマンス最適化 | 常に計算 | オンデマンド計算 |
| 構文 | { } クロージャリテラルが必要 |
通常の式を記述 |
▶ サンプル: オートクロージャによる遅延評価
// ============================================
// @autoclosure for deferred log output
// ============================================
var debugEnabled = false
func log(_ message: @autoclosure () -> String) {
if debugEnabled {
print("[DEBUG] \(message())")
} else {
print("Logging disabled, message skipped (not evaluated)")
}
}
// Even expensive string concatenation is not executed when disabled
debugEnabled = false
log("expensive " + "string " + "operation " + "skipped")
debugEnabled = true
log("this " + "will " + "be " + "logged")
出力:
TEXT 📖 参照専用Logging disabled, message skipped (not evaluated) [DEBUG] this will be loggedよくある落とし穴:
@autoclosureはパフォーマンス問題を隠しやすいです。本当に遅延評価が必要な場合のみ使用し、公開APIで乱用しないでください。過剰な使用はコードの可読性を低下させます。
5. キャプチャリストと循環参照
(1) 強参照サイクル
クロージャが外部変数をキャプチャする場合、デフォルトでは強参照です。クロージャとオブジェクトが互いに保持し合うと、強参照サイクル(循環参照)が発生します��
graph TB
A[ViewController] -->|強参照| B[クロージャプロパティ]
B -->|強参照| A
C[相互保持 → メモリリーク]
A --> C
B --> C
(2) キャプチャリストの構文
クロージャの先頭で [ ] を使ってキャプチャリ��トを宣言します:
| 宣言 | 意味 | ユースケース |
|---|---|---|
[weak self] |
弱参照、self がオプショナルになる |
最も一般的で最も安全 |
[unowned self] |
非所有参照、self は非オプショナルだが存続必須 |
self がnilにならない確信がある場合のみ |
[weak delegate = self.delegate] |
キャプチャ式 | self ではなく特定のプロパティをキャプチャ |
▶ サンプル: 循環参照とその解決策
// ============================================
// Closure retain cycles: weak vs unowned
// ============================================
import Foundation
class NetworkManager {
var onComplete: (() -> Void)?
func fetchData() {
// Simulate async request — escaping closure
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
print("Data fetched")
self.onComplete?()
}
}
deinit {
print("NetworkManager deinit")
}
}
class ViewController {
let manager = NetworkManager()
var data: String?
func loadData() {
// Use capture list to avoid retain cycle
manager.onComplete = { [weak self] in
guard let self = self else { return }
self.data = "New data"
print("UI updated")
}
manager.fetchData()
}
deinit {
print("ViewController deinit")
}
}
// Simulate usage and destruction
var vc: ViewController? = ViewController()
vc?.loadData()
// Release the view controller
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
print("Releasing ViewController")
vc = nil // Will not leak
}
出力:
TEXT 📖 参照専用Releasing ViewController NetworkManager deinit ViewController deinit Data fetchedヒント: 出力は
ViewControllerとNetworkManagerの両方が正しく解放されたことを示しています。クロージャ内の[weak self]が循環参照を防ぎます。asyncAfterコールバックが実行される時にはselfは既にnilなので、"UIが更新されました" は出力されません — 这正是我们望的安全行为です。
▶ サンプル: クロージャプロパティのよくある落とし穴
// ============================================
// Retain cycles in closure properties
// ============================================
class Counter {
var value = 0
var incrementHandler: (() -> Void)?
func setup() {
// Correct: use weak self to break retain cycle
incrementHandler = { [weak self] in
self?.value += 1
}
}
deinit {
print("Counter deinit")
}
}
var counter: Counter? = Counter()
counter?.setup()
print("Before release value = \(counter?.value ?? 0)")
counter = nil // Properly deallocated
print("Released")
出力:
TEXT 📖 参照専用Before release value = 0 Counter deinit Released
6. 完全な例:安全な非同期画像ローダー
// ============================================
// Complete example: Async image loader
// Features: @escaping + capture lists + memory safety
// ============================================
import Foundation
// 1. Image cache
class ImageCache {
private var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { return cache[key] }
func set(_ key: String, data: Data) { cache[key] = data }
deinit { print("ImageCache deinit") }
}
// 2. Image loader (using escaping closures)
class ImageLoader {
let cache = ImageCache()
func loadImage(from url: String, completion: @escaping (Data?) -> Void) {
// Check cache
if let cached = cache.get(url) {
completion(cached)
return
}
// Simulate network request — escaping closure runs async
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self = self else {
// self already deallocated, safely return
completion(nil)
return
}
// Simulate downloading data
let mockData = Data([0x01, 0x02, 0x03])
self.cache.set(url, data: mockData)
DispatchQueue.main.async {
completion(mockData)
}
}
}
deinit { print("ImageLoader deinit") }
}
// 3. Usage (safe release test)
var loader: ImageLoader? = ImageLoader()
loader?.loadImage(from: "https://example.com/photo.jpg") { data in
if let _ = data {
print("Image loaded successfully")
}
}
// Immediately release — weak self in closure guarantees no crash
loader = nil
print("Loader released, async callback won't crash")
// Keep running to wait for async completion
RunLoop.main.run(until: Date(timeIntervalSinceNow: 2))
出力:
TEXT 📖 参照専用Loader released, async callback won't crash ImageLoader deinit ImageCache deinit
❓ よくある質問
weak を優先してください。weak は参照をオプショナルにし、安全ですがアンラップが必要��す。unowned はオブジェクトが決して解放されないと仮定します — もし解放されるとアプリがクラッシュします。self がクロージャより長く存続すると100%確信できない限り、unowned は使わないでください。func foo(_ closure: @autoclosure () -> Void) に self.someMethod() を渡すと、クロージャが self を保持します。注意して使用してください。@escaping クロージャ内では、self へのすべての参照を明示する必要があります。これはSwiftが強制する構文上の注意喚起で、[weak self] が必要かどうかを考えさせるためのものです。重要なメモリ安全性設計です。📖 まとめ
@escapingは関数スコープを脱出できるクロージャをマークし、非同期コールバックやクロージャ保存に使用する@autoclosureは式を自動的にクロージャでラップし、遅延評価を実現する[weak self]を使ったキャプチャリストはクロージャとオブジェクト間の強参照サイクルを防ぐweakは参照をオプショナルにしアンラップが必要、unownedはオブジェクトが決して解放されないと仮定する- 非エスケーピングクロージャは安全に暗黙的な self を参照できるが、エスケーピングクロージャは明示的に self を参照する必要がある
guard let self = selfパターンで弱参照を安全にアンラップする
📝 練習問題
- 基本:
Stringと@escaping () -> Voidクロージャを受け取り、DispatchQueue.main.asyncAfterを使って1秒遅延実行するdelayPrint関数を書いてください。文字列とその文字列を出力するクロージャを渡して呼び出してください。 - 中級:
isEnabledがtrueの場合のみメッセージを出力するlog(_ message: @autoclosure () -> String)メソッドを持つLoggerクラスを書いてください。遅延評価の効果を示してください。 - 発展: エスケーピングクロージャの
[() -> Void]配列を内部に保存するTaskManagerクラスを作成してください。addTask(_:)とexecuteAll()メソッドを提供してください。ViewController で使用し、ViewController が解放されたときにメモリリークが発生しないようにしてください(キャプチャリストを使用する必要があります)。