📷

Swiftにおけるクロージャーのキャプチャリストを押さえる

2024/05/12に公開

はじめに

最近シャンプーとリンスの減りがバランス悪くなり、詰め替えに苦戦するNao-RandD です。

今回はSwiftのクロージャーでキャプチャリストに関して整理したいと思います。

関数型プログラミングが主流となりつつある昨今において、クロージャーの基礎をしっかりと押さえておくのはとても大切です。

早速ですが、以下のメソッドの出力はどのようになるでしょうか?

func createCounter() -> () -> Int {
    var count = 0
    let incrementer: () -> Int = { [count] in
        return count + 1
    }
    count = 10
    return incrementer
}

let counter = createCounter()
print(counter()) // <-- 出力は??

”こんなもの簡単だっ!”、クロージャーにキャプチャされているのだから、
「10」
と思った方、この後も記事を読み進めていただきたいです。

上記のコードの実行結果は、
「1」
になります。

どうして?

ちなみにコードを以下にすると、出力は 「10」 になります。

func createCounter() -> () -> Int {
    var count = 0
    let incrementer: () -> Int = { // [count] in <-- コメントアウト
        return count + 1
    }
    count = 10
    return incrementer
}

let counter = createCounter()
print(counter()) // <-- 出力は 「10」

この出力を理解するのにはSwiftのクロージャーの キャプチャリスト の挙動を理解する必要があります。

キャプチャリストとは?

キャプチャリストは、主には循環参照を避ける用途で用いられます。

みなさんも { [weak self] in ... }みたいにクロージャーを定義して、キャプチャするクロージャーへのself参照が、循環しないようにしたことがあるかと思います。

クロージャーはキャプチャという強力な力がある反面、そのまま参照型の変数を扱うと強参照を持ちます。

なので、クラスインスタンスのプロパティにクロージャーを割り当て、そのクロージャー内でインスタンスをキャプチャする場合などに参照への考慮が抜けると、循環参照が起きてしまうのですね。

この辺りのドキュメントが参考になります。

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting#Defining-a-Capture-List

本題の挙動

今回の例で示したコードではキャプチャした変数は参照型ではありませんでした.

値型のIntの変数をクロージャーで扱っています。

【おまけ】扱うのが参照型のクラスだった場合は?

もし仮に扱うコードでcountが参照型のクラスプロパティであったなら、キャプチャした変数は参照を持つためキャプチャリストがあってもなくても出力は同じです。

以下のコードはどのような出力になるでしょうか?

class Counter {
    var count: Int

    init(count: Int) {
        self.count = count
    }
}

func createCounter() -> () -> Int {
    var counter = Counter(count: 0)
    let incrementer: () -> Int = { [counter] in
        return counter.count + 1
    }
    counter.count = 10
    return incrementer
}

let counter = createCounter()
print(counter()) // <-- 出力は??

答えは 「11」 です。

counterインスタンスのcountプロパティは0で初期化され、counter.count = 10で10になり、クロージャーを実行したタイミングで +1されるので、11になるんですね。

コードの出力を正確に読み取るためにも、キャプチャだけでなく実行タイミングなどもしっかり意識して日々コードと向き合いたいですね。

値型に対するキャプチャリストのある・なし

キャプチャリストが指定されていない場合、クロージャはそのスコープ内の変数を自動的にキャプチャします。

この場合、クロージャは変数の最新の値を「参照」しますが、これは値型に対してもう少し正確には「最新の値を反映する」と言う表現になるかと思います。(参照型の参照とは意味合いが厳密には異なると思っている、、)

より正確な表現あればコメントで教えてください🙇‍♂️

func createCounter() -> () -> Int {
    var count = 0
    let incrementer: () -> Int = { [count] in // <-- ここでcount = 0のスナップショットをとる
        return count + 1
    }
    count = 10
    return incrementer
}

let counter = createCounter()
print(counter()) // 出力:1

キャプチャリストを使って値型をクロージャにキャプチャすると、クロージャが作成された時点でのその値の「スナップショット」が取られます。これは、その時点での値のコピーがクロージャによって保存されることを意味します。

キャプチャリストによってスナップショットとして保持されているか否かによって、今回の出力の違いが出ているというわけですね。

まとめ

今回は意外とサラッと見たら間違えそうな、キャプチャリストの挙動を紹介しました。

僕は見事に引っかかり、鬱屈する気持ちを成仏させるために記事にさせていただきました。

昨今は関数型プログラミングが主流となりつつあり、クロージャーを正確に理解して扱う力は大切かと思います。

誰かのお役に立てたなら本望です。

参考

https://youtu.be/DMIG8aDNM9I?si=KVBNIrkxd_ZPXIns

Discussion