Swift: Swiftクロージャ応用:エスケーピングクロージャ、オートクロージャと循環参照

クロージャは非同期プログラミングの中心的な役割を果たしますが、ライフサイクル管理とメモリリークの課題ももたらします。このレッスンではクロージャの高度な使い方を深く掘り下げ、複雑なアプリケーションシナリオで安全かつ効率的に使用できるようにします。

1. 学習目標


2. モバイル開発者の実話

(1) 課題:ネットワークコールバックのクラッシュとメモリリーク

Charlieはユーザーがコメントを投稿するソーシャルアプリを構築しており、API呼び出し後にUIを更新する必要があります:

SWIFT
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 + キャプチャリスト

SWIFT
// 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 をマークすると、関数のスコープを脱出できるようになります。

100%
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管理が必要
パフォーマンス より良い わずかな追加オーバーヘッド

▶ サンプル: エスケーピングクロージャの使用

SWIFT
// ============================================
// 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 は式を自動的にクロージャでラップし、遅延評価を可能にします:

100%
graph TB
    A["assert(condition: 2 > 1)"] --> B["通常評価: 即座に計算"]
    C["assert(condition: 2 > 1, message: \"error\")"] --> D["@autoclosure: 条件が偽の場合のみ評価"]
    D --> E["文字列連結のオーバーヘッドを回避"]
シナリオ 通常パラメータ @autoclosure
評価タイミング 呼び出し箇所で即座に評価 クロージャが呼ばれた時のみ評価
パフォーマンス最適化 常に計算 オンデマンド計算
構文 { } クロージャリテラルが必要 通常の式を記述

▶ サンプル: オートクロージャによる遅延評価

SWIFT
// ============================================
// @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) 強参照サイクル

クロージャが外部変数をキャプチャする場合、デフォルトでは強参照です。クロージャとオブジェクトが互いに保持し合うと、強参照サイクル(循環参照)が発生します��

100%
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 ではなく特定のプロパティをキャプチャ

▶ サンプル: 循環参照とその解決策

SWIFT
// ============================================
// 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

ヒント: 出力は ViewControllerNetworkManager の両方が正しく解放されたことを示しています。クロージャ内の [weak self] が循環参照を防ぎます。asyncAfter コールバックが実行される時には self は既にnilなので、"UIが更新されました" は出力されません — 这正是我们望的安全行为です。

▶ サンプル: クロージャプロパティのよくある落とし穴

SWIFT
// ============================================
// 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. 完全な例:安全な非同期画像ローダー

SWIFT
// ============================================
// 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

❓ よくある質問

Q @escaping はいつ必ず使う必要がありますか?
A クロージャが外部変数(配列やプロパティなど)に保存される場合や、非同期コールバック(DispatchQueue、URLSession)で使用される場合は、@escaping をマークする必要があります。コンパイラがこれを強制します。
Q weak と unowned の選び方は?
A weak を優先してください。weak は参照をオプショナルにし、安全ですがアンラップが必要��す。unowned はオブジェクトが決して解放されないと仮定します — もし解放されるとアプリがクラッシュします。self がクロージャより長く存続すると100%確信できない限り、unowned は使わないでください。
Q @autoclosure は循環参照を引き起こしますか?
A はい、可能性があります。@autoclosure は暗黙的に外部変数をキャプチャするためです。例:func foo(_ closure: @autoclosure () -> Void)self.someMethod() を渡すと、クロージャが self を保持します。注意して使用してください。
Q 非エスケーピングクロージャに [weak self] が不要な理由は?
A 非エスケーピングクロージ��は関数が返る前に実行を完了し、self のライフサイクルより長く存続しません。Swiftコンパイラはこれを認識し、暗黙的な self 参照を許可します。これは非エスケーピングクロージャの安全上の利点の一つです。
Q エスケーピングクロージャ内では self を明示的に書く必要がありますか?
A はい。@escaping クロージャ内では、self へのすべての参照を明示する必要があります。これはSwiftが強制する構文上の注意喚起で、[weak self] が必要かどうかを考えさせるためのものです。重要なメモリ安全性設計です。

📖 まとめ


📝 練習問題

  1. 基本: String@escaping () -> Void クロージャを受け取り、DispatchQueue.main.asyncAfter を使って1秒遅延実行する delayPrint 関数を書いてください。文字列とその文字列を出力するクロージャを渡して呼び出してください。
  2. 中級: isEnabled がtrueの場合のみメッセージを出力する log(_ message: @autoclosure () -> String) メソッドを持つ Logger クラスを書いてください。遅延評価の効果を示してください。
  3. 発展: エスケーピングクロージャの [() -> Void] 配列を内部に保存する TaskManager クラスを作成してください。addTask(_:)executeAll() メソッドを提供してください。ViewController で使用し、ViewController が解放されたときにメモリリークが発生しないようにしてください(キャプチャリストを使用する必要があります)。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%