Swift & Objective-C

[Swift基礎 #2] クロージャのキャプチャとweak self、escaping

Swiftコードでクロージャは空気のような存在です。ソート条件、ネットワークの完了ハンドラー、ボタンアクション、SwiftUIのbodyまで、中括弧のブロックを渡すコードは一日に何十回も登場します。それでも「クロージャが値をキャプチャするとは正確にどういうことか」「[weak self]は…

読了 8 分
[Swift基礎 #2] クロージャのキャプチャとweak self、escapingのカバー画像

Swiftコードでは、ソート条件、ネットワークの完了ハンドラー、ボタンアクション、SwiftUIのbodyまで、クロージャがいたるところに登場します。しかし「クロージャが値をキャプチャするとは何か」「[weak self]はなぜ使うのか」「@escapingはなぜ付けるのか」と聞かれると、説明に詰まる人も少なくありません。

この3つの疑問は、実は一つにつながっています。クロージャが周囲の値をキャプチャする仕組みを理解すれば、参照型である理由、循環参照が起きる理由、escapingの指定が必要な理由まで連続して理解できます。Swift基礎シリーズ第2回では、そのつながりを順に見ていきます。

クロージャとは — 名前より重要な「捕まえる」性質

まず構文を最小限に整理します。クロージャは実行コードのブロックを値として扱う構文です。変数に格納し、引数として渡し、返すことができます。

let add: (Int, Int) -> Int = { a, b in a + b }
add(2, 3) // 5

実は、funcで宣言する関数も名前付きクロージャです。Swiftでは関数とクロージャは同じ仲間で、{ }構文は名前を付けずその場で作る形にすぎません。

closureという名前は「コードブロック」ではなく、周囲の変数を包み込んで閉じる(close over)性質に由来します。

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3

不思議なことが起きています。countはmakeCounterのローカル変数なので、関数が返れば消えるはずです。それなのに返されたクロージャを呼ぶたび増え続けます。クロージャが自分の外側にある変数countをキャプチャし、関数終了後も生かしているからです。これがクロージャの本質で、この記事の残りはすべてこのキャプチャから派生します。

キャプチャの正確な意味 — コピーではなく参照

Swiftクロージャの基本のキャプチャ方式は参照です。値をコピーするのではなく、その変数自体とのつながりを保ちます。そのため次の結果になります。

var multiplier = 2
let times = { (n: Int) in n * multiplier }

times(10)      // 20
multiplier = 3
times(10)      // 30 — クロージャは変更後の値を見る

使われたのは、クロージャ作成時のmultiplier(2)ではなく、実行時のmultiplier(3)です。クロージャは変数のスナップショットではなく、変数そのものを持ち運びます。

作成時点の値を固定したい場合は、キャプチャリストを使います。

let times = { [multiplier] (n: Int) in n * multiplier }

times(10)      // 20
multiplier = 3
times(10)      // 20 — 作成時点の 2で固定

角括弧内に書いた変数はクロージャ生成時にコピーされ、クロージャ内の定数として固定されます。まとめると、基本は参照キャプチャ(生きたつながり)、キャプチャリストは値キャプチャ(生成時点のスナップショット)です。[weak self]を説明する前に、この区別を押さえる必要があります。

このキャプチャ用ストレージがあるため、クロージャは参照型です。キャプチャされた変数はクロージャと同じ寿命を持つ必要があるためヒープに保存され、クロージャ値をコピーすると同じストレージを共有する参照が一つ増えます。struct中心のSwiftでクロージャがクラスのように振る舞う理由です。値型と参照型の区別に慣れていなければ、まず値型優先主義の記事を読んでください。

基本のキャプチャは生きたつながり、キャプチャリストは生成時点のスナップショット
基本のキャプチャは生きたつながり、キャプチャリストは生成時点のスナップショット

循環参照と[weak self] — キャプチャが生む典型的な事故

参照キャプチャの代償が循環参照です。構図はいつも同じです。

class ProfileViewModel {
    var onUpdate: (() -> Void)?
    var name = ""

    func bind() {
        onUpdate = {
            print("名前: \(self.name)")
        }
    }
}

ビューモデルはonUpdateプロパティを通じてクロージャを強く所有します。一方、そのクロージャはself、つまりビューモデルを参照キャプチャしています。ビューモデル → クロージャ → ビューモデルという所有の輪が閉じ、ARCの世界では互いを保持したまま永遠に解放されません。画面を閉じてもビューモデルがメモリに残り、リークが完成します。

解決策は、キャプチャリストの2つ目の用途です。[weak self]はselfを弱参照でキャプチャする指示です。クロージャはselfを保持しますが所有しないため、輪が切れます。その代わりselfが先に解放される可能性があるので、クロージャ内のselfはオプショナルになり、通常はguard let self else { return }から始めます。オプショナルの記事で扱った早期脱出パターンがここでも登場します。

onUpdate = { [weak self] in
    guard let self else { return }
    print("名前: \(self.name)")
}

重要なのは、[weak self]が万能な接頭辞ではないことです。循環には「selfがクロージャを所有し、そのクロージャがselfをキャプチャする」という条件が必要です。この条件がなければweakも不要です。たとえばDispatchQueue.main.asyncAfterに渡すクロージャは実行後にシステムが破棄するため、selfが少し長く生きるだけでリークではありません。反射的にweakを付けるのではなく、「このクロージャを誰がどれだけ長く所有するか」と考えるのが正確な習慣です。NSTimerのように所有構造が特殊で有名な例は別の記事で扱います。

@escaping — クロージャが関数より長く生きるとき

最後のピースです。関数パラメーターとして受け取るクロージャには、ときどき@escapingが付きます。

func fetchUser(completion: @escaping (User) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, _ in
        let user = parse(data)
        completion(user)   // 関数が返ったずっと後に実行される
    }.resume()
}

基準は実行時点です。関数が返る前に内部で実行して破棄するクロージャはnon-escaping(デフォルト)です。プロパティに保存したり非同期処理に渡したりして、関数の返却後に実行される可能性があればescaping、つまり関数の外へ逃げるクロージャです。

なぜ区別を強制するのでしょうか。クロージャがescapingかどうかで、コンパイラーと開発者の考え方が変わるからです。non-escapingは関数実行中だけ生きることが保証されるため、コンパイラーはキャプチャストレージを最適化でき、循環参照の心配も根本的にありません。mapやfilterに渡すクロージャでselfを気にしなくてよいのはそのためです。一方escapingはクロージャがどこかに保存され長く生きることを意味するため、前述の循環参照の確認対象になります。@escapingは「このクロージャは長く生きるのでキャプチャに注意」というAPIレベルの警告ラベルです。

completionハンドラー形式のescapingクロージャは、async/awaitの導入により新しいコードでは減少傾向にあります。それでも既存APIを読んだりブリッジしたりするには、正確に理解しておく必要があります。

互いを保持する輪をweakが断ち切る
互いを保持する輪をweakが断ち切る

実践で使う3つの基準

理論を実務向けにまとめると、基準は3つです。

**1つ目、クロージャを見たらまず寿命を考えます。**関数内で消費されて終わるのか(non-escaping)、どこかに保存されて長く生きるのか(escaping)。この一問で、キャプチャをどれだけ気にすべきかが決まります。

**2つ目、weakは反射ではなく判断で使います。**プロパティに保存するハンドラーやデリゲート的コールバックなど、所有の輪が実際に閉じる場所には[weak self]を使います。輪のない一度きりの実行には不要です。判断に迷ったら、InstrumentsのLeaksやdeinitログで確認する習慣を付けましょう。

**3つ目、キャプチャリストで意図を文書化します。**値を固定したいなら[value]、所有したくないなら[weak self]です。キャプチャリストは性能の仕組みである以前に、「このクロージャが外の世界とどうつながるか」をコードに明示する文書です。

まとめ

  • クロージャの本質はコードブロックではなくキャプチャ、つまり周囲の変数を包み込んで閉じ、寿命を延ばす性質です。
  • 基本のキャプチャはコピーではなく参照なので、クロージャは実行時の値を見ます。キャプチャリスト[x]は生成時点の値に固定します。
  • キャプチャストレージを共有する必要があるため、クロージャは参照型です。
  • selfが所有するクロージャがselfをキャプチャすると循環参照になります。[weak self] + guard let selfが標準解です。ただし所有の輪がなければweakも不要です。
  • @escapingは「関数より長く生きるクロージャ」を示す警告ラベルで、キャプチャを確認すべき箇所を知らせます。

次回は、保存プロパティと計算プロパティ、lazy、プロパティオブザーバーまで、変数宣言1行に隠れた選択肢を整理します。

あわせて読みたい