SwiftのクロージャとselfのキャプチャをC++ラムダ感覚で整理する

Swiftのクロージャは、C++のラムダを知っていると入りやすいです。

ただ、self のキャプチャや @escaping が出てくると少し詰まります。

公式では、closureはコード内で渡したり使ったりできる自己完結した機能ブロックとして説明されています。

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/

基本のクロージャ

let values = [1, 2, 3]
let doubled = values.map { value in
    value * 2
}

短く書くならこうです。

let doubled = values.map { $0 * 2 }

C++のラムダならこの感覚です。

std::ranges::transform(values, output, [](int value) {
    return value * 2;
});

trailing closure

Swiftでは、最後の引数がクロージャの場合、括弧の外に出せます。

values.map { value in
    value * 2
}

SwiftUIでよく見るこの形も同じです。

Button("Load") {
    viewModel.load()
}

最初は構文が独特ですが、「最後の関数引数が後ろに出ている」と見ると読みやすいです。

escaping closure

関数が受け取ったクロージャを、関数の終了後にも保持する場合は @escaping が必要です。

func later(_ action: @escaping () -> Void) {
    DispatchQueue.main.async {
        action()
    }
}

関数の外へ逃げるのでescapingです。

非同期処理やコールバックで出てきます。

selfを明示する場面

escaping closureの中でインスタンスメンバを使う場合、self を明示します。

final class Loader {
    var count = 0

    func start() {
        later {
            self.count += 1
        }
    }
}

これは「selfをキャプチャしている」ことを見えるようにするためです。

C++のラムダで [this] を書く感覚に近いです。

[this] {
    count += 1;
};

weak self

クロージャがselfを強く持つと、循環参照になることがあります。

final class ViewModel {
    var onFinish: (() -> Void)?

    func setup() {
        onFinish = { [weak self] in
            self?.reload()
        }
    }

    func reload() {}
}

[weak self] は、selfを弱参照でキャプチャします。

Swiftでは、classの参照はARCで管理されます。クロージャとインスタンスがお互いを強く持つと解放されないことがあります。

まとめ

SwiftのクロージャはC++ラムダに近いですが、キャプチャの見え方が少し違います。

  • { value in ... } がクロージャ
  • $0 で省略できる
  • 最後のクロージャはtrailing closureで外に出せる
  • 関数の外へ残るなら @escaping
  • escaping closure内の self は明示する
  • 循環参照がありそうなら [weak self]

SwiftUIや非同期処理ではかなり頻繁に出るので、@escapingself の関係だけ先に押さえておくと楽です。

参考