本日も引き続き クロージャー
における 強参照循環
の解消方法についてを見ていきますね。そんな中でもとりわけ キャプチャーリスト
がメインテーマになりそうです。微妙にややこしいところでもあるので、今回はその特色的なところを中心に話していけたらいいなと思っています。よろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #313
00:00 Start 00:12 今回の展望 02:41 かんたんに、おさらい 03:47 キャプチャーリストの定義方法 06:39 キャプチャーリストとクロージングオーバー 12:18 普通のキャプチャーとクロージングオーバーの違い 18:48 参照型をキャプチャーしたときの挙動 23:40 並行処理を意識したキャプチャーリストの扱い 27:32 Swift Concurrency とキャプチャーリスト 31:50 クロージングと次回の展望 ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #313
今日は、クロージャーにおける循環参照の解消方法について考えていきます。この解消方法の一つとして、キャプチャリストを使う方法があります。これは、プログラマーには比較的おなじみのやり方かもしれません。しかし、私の個人的な意見としては、キャプチャリストが無駄に使われている印象を受けることがしばしばあります。それでも過剰に使うことで大きな問題が起きるわけではないので、安全策として採用するのは良いかもしれません。ただ、美しさを求めると、ちょっと違うかなという気もします。
この勉強会を通じて、キャプチャリストについてしっかり理解し、過剰な使用を避けるのはもちろん、安全性を確保しつつもコードの美しさを意識して、実際のアプリケーションやプロダクトで取るべきバランスを学びたいと思います。このようにしっかり理解を深めると、どのようにキャプチャリストを中心に考えるか、面白く見えてくるのではないかと思います。
さて、前回少しお話ししましたが、クロージャーがself
をキャプチャーし、そのクロージャーをself
が保持すると、循環参照が発生します。そのため、self
をキャプチャーする場合は明確にself
を書く必要があります。Swiftでは通常、self.
を省略しますが、この場合はそうするわけにはいきません。キャプチャの一つの特徴として、明確に理解しておく必要があります。
次に、キャプチャリストの定義方法について確認します。キャプチャリスト内でアイテムを宣言する場合、インスタンスそのものやクラスインスタンス(もちろん値型もキャプチャできます)をweak
やunowned
などで指定して、インスタンスをどのように扱うかを決定することができます。例えば、あるデリゲートプロパティをキャプチャする場合、self.delegate
のように書くことができます。キャプチャリストの記述方法は、カッコでくくり、カンマで区切る形で記述します。
具体的に言えば、次のように書きます。クロージャーのブロックがあって、その最初にキャプチャリストを[weak self] in
のように書くことができます。これにより、クロージャー内でself
をweak
参照としてキャプチャすることができ、循環参照を防げるようになっています。普段、この構文をそれほど多用しない場合は、時々忘れてしまうかもしれませんが、必要に応じて活用するようにしましょう。 キャプチャーリストを定義することができて、使っていくことができるということですね。今日はクロージャーについて整理しておきました。意外と何気なく使っていると分かりにくい動きがあったりするので、ここは整理しておくと循環参照だけでなく、いろいろと役に立つのではないかと思います。
プレイグラウンドでクロージャーのキャプチャーリストを整えていきますね。クロージャーは重要な特徴として、パラメーターやプロパティをキャプチャーできるという特性があります。よくある例ですが、例えば var value = 0
として、let increment = { value += 1 }
というクロージャーを定義します。このクロージャー内で値を変更し、print(value)
で increment()
を呼び出すと、バリューがインクリメントされていくという具合です。
このクロージャーを設定した段階では、まだ呼び出していないのですが、value
が内包されており、それを変えることができます。結果が出ることを期待していると、例えば 0, 1, 2
というふうにインクリメントされた結果になるはずです。
このようにクロージャーがアクセスできるスコープにある変数をキャプチャーして、その値を変更することができます。後で必要なタイミングでそれを利用できるのが面白いところです。この性質を活かすと、関数 getIncrementFunction
のような形で、Void
を返す関数を返すようにして、return increment
とすると、let increment = getIncrementFunction()
としてこのインクリメントを利用することができます。
興味深いのは、通常であれば関数内で定義された変数は関数を抜けた後には解放されるはずですが、インクリメントを返した時に、そのクロージャー内で value
が使われることにより、その変数の生存期間が延長されるという特殊な動きを示すことです。これがクロージャーによるキャプチャーの面白いところです。
一般的にこういったものをキャプチャーと言いますが、詳しく見ると「クロージングオーバー」によるキャプチャーとキャプチャーリストによるキャプチャー、この二つが異なる動きを示します。 例えば、Swiftでプログラムを書いていて、キャプチャーリストを使うとエラーが出ることがあります。キャプチャーリストを使うというのは、基本的に関数のスコープ外にある変数を関数内部で使用する際に、特定の値をクロージャ内で保持する仕組みです。
ここで重要なのは、一度キャプチャーリストを使って変数を捕捉すると、その値はイミュータブル、つまり変更不可能なものとして扱われます。ですので、キャプチャーリストを経由して代入された変数を変更しようとしても、変更は反映されません。例えば、以下のようにして変数を捕捉して、その後変更を試みた場合には、キャプチャーの時点の値が保持され続けます。
let value = 0
let closure = { [value] in
print(value)
}
closure() // 実行結果は0
// ここでvalueに1000を加算しても
value += 1000
// 再度クロージャを呼び出しても
closure() // 実行結果は0のまま
この例のように、キャプチャーリストを持つクロージャを使用すると、その変数の値は固定され、後からの変更はキャプチャされた値には影響を与えません。
一方で、キャプチャーリストを使わずに変数を捕捉した場合には、その変数の実際の値を参照し続けるので、変更がクロージャの実行に反映されることになります。これがクロージャが変数をオーバークロージャーする(閉じ込める)という動作の違いです。この動作の違いを把握しておくことが重要です。
このような点を理解しておけば、プロジェクトで意図しない動作に遭遇した際にも、冷静に対処できるでしょう。特にSwiftのプログラミングでは頻繁にクロージャを扱うため、この知識は非常に有用です。 プラスイコールやローバリュー、そしてプラスイコール演算子については慎重に扱うべきです。カスタムストリングコンバーティブルのコメントにも、description
プロパティを直接書くべきでないという趣旨のことが書かれていることがあります。このdescription
プロパティについては、今回使用するかどうかで迷うこともありますが、使わない方針であればストリングのディスクリプションと等価な動作を実現する方法を検討することが求められます。
さて、これで定列表現が正しく出力されるようになったことを確認できたら、結果が今までどおり表示されるか確認します。これによって、期待どおりのキャプチャー挙動が実現され、キャプチャーした後でも変数の値が反映されていることを確認できます。
キャプチャーリストを利用することで、クロージャで変数をキャプチャした際の挙動について詳しく理解しておくべきです。特に、キャプチャされた変数の参照先が共通であることから、外部からインスタンスを変更した場合でも、キャプチャされている変数の参照先も変わるという動作が起きます。これによって、値が変わったことを確認できるのです。このため、キャプチャーする際には不意のバグにつながらないよう注意を払うべきです。
逆に、この挙動を利用して意図的にバグを再現することができるかもしれません。例えば、構造体 (struct
) を使用することによって、コピーされるためキャプチャーした値が期待通りに動かない場合があります。結果プログラムの挙動を確認しつつ、さらにディスパッチやコンカレンシーを利用する方法でも検証が行えます。
例えば、DispatchQueue.global().async
を用いて別スレッドにクロージャを渡す場合、キャプチャした変数はコピーされて渡るため、その後に変数を書き換えたとしても、外のスレッドで実行されたときの変数とバッティングしません。これにより不正アクセスやデータの競合が防がれるのです。キャプチャリストを適切に使用すると、スレッドセーフを考慮した実装が可能となります。
キャプチャリストの理解と活用は、特にスレッドをまたぐ際の安全性において役立つ可能性があります。これまで実際にクロージャをこのように意識して作ったことがない場合でも、学んでおくと意外なところで用立てられるかもしれません。大事なのは、しっかりとした理解とそれに基づいた正確な実装です。 コンカレンシーについて考えてみましょう。例えば、あるアプリケーションで sync
メソッドを使用してクロージャーを取る場合を考えます。ここで、コンパイルエラーになるかどうかという疑問が出てきます。たしか、Sendable
が必要でしたよね。実行しないといけませんでしたっけ。ちょっと忘れてしまいましたが、sync
関数に一旦 F を渡してみましょう。
Await
サウンシング ストリーミングになってしまったので、ここで F を使ってみるとどうなるでしょうか。また、Execution Failure
というエラーが出たのは無視して、どうしようか考えます。Sendable
が必要だったんだっけな、と曖昧に覚えていますが、どうもキャプチャーしなければ Sendable
は要求されなさそうという気がしました。コンパイラを通してみることにしましょう。
ビルドをかけてみて、Sendable
クロージャーがどういう時に必要なのかが気になります。これは Swift の中では結構重要だと思います。もし間違っているところがあれば、ぜひ教えてください。問題を整理してコンパイルしてみると、await
で F が Sendable
クロージャーでないとダメということです。これを Sendable
にする必要があるのですね。
バリューがノンアイソレーティングだからダメという警告ですが、これを直せばビルドは通ります。バリューを1個にするとアイソレーティングになるなど、そうした微修正で警告もエラーもなくビルドが通るようになります。
また、プロジェクトが三つあり、ターミナルアプリ、プレイグラウンド、iOS アプリ向けのものがあります。最初の警告が出なかったのは iOS のプレイグラウンドですが、警告が出るようになったのはターミナルアプリのソースコード部分です。これはビルドセッティングで、コンカレンシーのレベルを調整してあるので、ターミナルアプリでエラーが出るようになったのです。
キャプチャリストを使用する方法もあり、これは非常に重要なポイントです。キャプチャリストについてはおおよそ理解できたようなので、次回はこれを踏まえてクロージャーの循環参照の解消について実際に見ていこうと思います。以上で今回の内容を終わりにします。お疲れ様でした。ありがとうございました。