前回に クロージングオーバー
の話に逸れましたけれど、そこから エスケーピングクロージャー
との関連性を投げかけてもらったのをきっかけに Swift Concurrency とクロージャーとの関係性で不思議に思うところがありました。それがなかなか興味深かったので、今回はまずそのあたりについて改めて見ていくことにしますね。
それが終わったら引き続き、オプショナルバインディング
の if var
表記や 強制アンラップ
周りを見ていこうと思います。これらはこれまでにもたびたび触れた話題ですけれど、おさらいとしてそれに主眼を置いて眺めてみようと思います。
今回もゆめみ社外の人を招いての開催で、一般の方も若干名参加してくださる見込みです。どうぞよろしくお願いしますね。
———————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #164
00:00 開始 00:10 今回の展望 02:13 今回の話を聞く上での心持ち 04:08 クロージングオーバーのおさらい 07:10 C 言語の static 変数 08:32 エスケーピングクロージャー 12:45 クロージャーの実行を先送りするには配慮が必要 14:01 生存範囲を超えてクロージャーを存続 15:23 キャプチャーとクロージングオーバーの違いを確かめる 21:13 @escaping による影響について 25:19 ローカルで定義したクロージャーは @escaping としても使える 26:03 クロージャーの生存が延命される様子 28:08 Concurrency が作る生存範囲 31:15 await による中断で延命される 32:49 今回の所感とクロージング ————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #164
今日は、前回の勉強会の続きから始めますね。前回はイフレットの省略表記について話をしましたが、今日は別のトピック、クロージャーやシャドウイングに関する話題からいきます。
話を進める中で、クロージャーとクロージングオーバー、キャプチャについても解説しました。特に「クロージングオーバー」なんて言葉は、一般的にはあまり知られていないですよね。書籍にはこちらの用語が使われることがありますが、実際にどれだけの人が使っているかは分かりません。ただ、こういった用語が注目されることで、理解が深まることもあります。
クロージャーのクロージングオーバーとキャプチャの違いについて話した後、次にエスケーピングクロージャーの話になり、そこからSwiftのコンカレンシーについても触れました。この分野に関する知識をさらに掘り下げることで、自分が見落としていた点に気づくことができ、大変興味深かったです。ですので、今日はそのあたりについて詳しく話していきたいと思います。
いくつかの周辺知識が要求されるかもしれませんが、できるだけ丁寧に解説していきます。難しいと感じるところがあっても、それがどこなのかだけでも覚えておいてもらえれば、その後の学習のきっかけになると思います。ですから、分からない部分があっても、その言葉や雰囲気を感じ取ってもらうだけでも十分です。自分のレベルに合わせて聞いてください。また、質問が浮かんだら積極的に聞いてもらえればと思います。
さて、前回見たのが「キャプチャ」と「クロージングオーバー」の違いです。例えば、func increment()
というメソッドがあったとします。そこで、increment
を使ってシリアルナンバーを生成する関数 generateSerialNumberFunction
を作ってみましょう。
まず、シリアルナンバーを1から始めるとします。そのために、以下のようにしてバーを定義します。
var serialNumber = 0
return { () -> Int in
serialNumber += 1
return serialNumber
}
このコードは、シリアルナンバーを生成するクロージャーを返すものです。このようにして生成された関数を makeSerialNumber
という名前にしておきます。この関数を呼び出すたびにシリアルナンバーがインクリメントされます。
このときに関数がキャプチャしている変数 serialNumber
は、関数外でも状態が維持され続けます。普通だったらこのローカルスコープを抜けたタイミングで変数は消えるはずですが、クロージャーがキャプチャしていることで生き続け、その外でも利用されるというわけです。
この概念は、C言語を使っていた人には少しなじみがあるかもしれません。C言語では、「スタティック変数」という形で似た動きをする変数を使えます。以下のように書き方は異なりますが、概念としては近いものがあります。
static int serialNumber = 0;
serialNumber += 1;
ただし、C言語とは動作の細部が異なりますので、一度きちんと調べる価値があります。興味がある方はぜひ深掘りしてみてください。
では、続けて他の話題に入りましょう。 このように動くかどうかは、キャプチャリストにしたり、思うように動かなくなることがあったり、書き換えられないことが影響しています。この話に関連して、クロージャーのキャプチャと似ていることについての雰囲気を話すことがあります。ここからは、クロージャーの特徴について説明します。
今までの話が分からなかった人は忘れても大丈夫です。例として、関数型の変数を考えてみましょう。例えば何も取らず、何も返さない関数が変数として用意されていたとします。この場合、print
関数が入っているとしましょう。次に、クロージャーとして Void
を取って Void
を返す関数を受け取る関数 function
を作り、これを クロージャー g
として呼び出すコードを書いたとします。
let printFunction: () -> Void = {
print("Hello, Swift")
}
func executeClosure(_ closure: @escaping () -> Void) {
closure()
}
executeClosure(printFunction) // "Hello, Swift" と出力されます
このコードでは、関数 executeClosure
に printFunction
を渡すと、print
が呼ばれることになります。
さて、これをオブジェクトにしてみる場合、例えば struct
でクロージャーをプロパティに持つようにしてみましょう。
class MyObject {
var storedClosure: (() -> Void)?
func prepare(closure: @escaping () -> Void) {
self.storedClosure = closure
}
func invoke() {
self.storedClosure?()
}
}
let myObject = MyObject()
myObject.prepare {
print("Hello, Struct")
}
myObject.invoke() // "Hello, Struct" と出力されます
このように、MyObject
クラスはクロージャーをプロパティ storedClosure
に保持し、後で invoke
メソッドを使って呼び出します。このとき、クロージャーを保存するために @escaping
キーワードを使います。これは、クロージャーが関数のスコープを超えて存続することを示しており、そのような動作が必要な場合は @escaping
をつけることで、コンパイラーに意図を伝えることができます。
まとめると、クロージャーが関数の外で生き続ける場合、@escaping
キーワードを使ってそのポテンシャルなリスクを示す必要があります。これにより、コードの読みやすさと安全性が保たれます。 なので、この点については注意が必要です。これによってクロージャを変名でき、スコープを超えて存続させることができます。先ほどの「クロージングオーバー」と似た表現になってきますが、このあたりはどう捉えるかによって違ってくると思います。ですので、あまりこだわらないほうがいいと思います。あくまでスコープを超えてクロージャを存続させる仕組みが用意されているということです。
ここで少し余談になりますが、前回の勉強会で「weak self」のような弱参照とクロージャの参照、つまりクロージャの「escaping」と「non-escaping」に類似点を見たという話がありました。それは確かにその通りで、自分もあまりそういった視点を持っていなかったので、とても興味深かったです。このおかげで視野が広がりました。実際に「weak self」との関係性を改めて考えると、「escaping」をなぜ使用するかが理解しやすくなります。
もともとクロージャは参照型です。参照型であるものをスコープを超えて存在させる場合、参照が複数あればスコープを超えて存在できます。ですので、わざわざ「escaping」と指定しなくても一応参照型としては機能するわけです。同様に、他のインスタンスにわざわざ「これはエスケーピングだ」と指定しないのと同じです。これを理解すると不思議に思えるかもしれませんが、非常に興味深い部分です。
例えば、あるインスタンスがあり、そのインスタンスをクロージャでキャプチャする場合について考えてみます。具体的には例えば NSString
をキャプチャして処理をする場合です。クロージャの中で NSString
を操作する例として、以下のように書きます。
let myString = NSMutableString(string: "Hello")
let closure = {
myString.append(" World")
}
closure()
print(myString)
上記の例では、myString
はクロージャの外に定義された変数ですが、クロージャ内で操作しています。これが「クロージングオーバー」の一例です。
ただし、この例は理解を深めるためのもので、実際にはコンパイラやランタイムの振る舞いによって結果が異なる場合もあります。特に NSMutableString
の操作などは、データの管理に注意が必要です。何らかの理由でキャプチャしたインスタンスが意図しない状態になることがあるため、万全を期すために具体的な実装に際しては注意を払う必要があります。例えば NSMutableString
の場合、「append」を使うことで文字列を追加できることが確認できます。
このように、クロージャと型の関係性やスコープの管理方法を理解することは、Swiftにおける重要な知識になります。 例えば、Swiftでクロージャを使用する場合、特にキャプチャリストがなくても、参照型はキャプチャされます。参照型のインスタンスをキャプチャするかどうかが重要です。キャプチャリストを使わないと、すべてのクロージャで強参照されるため、場合によってはメモリリークが発生することがあります。
今回は参照型でキャプチャする方法について説明します。ウィーク(weak
)やアンオウンド(unowned
)のインスタンスとしてキャプチャすると、クロージャがスコープを超えて存在する場合でもメモリリークを防ぐことができます。つまり、クロージャが関数Fの外部で使用されても、そのFが開放された場合に、クロージャ内でキャプチャされている参照型のインスタンスにも影響を与えます。これがスコープ外で存続するかどうかによって変わります。
Escapingクロージャについて話を戻しましょう。例として、関数Fがあるとします。このFがアクション後も使用される可能性があるかどうかを考慮します。通常、このFはスコープ内で定義され、アクションに渡され、そのスコープが終了する時に解放されます。しかし、Dispatch Queueのグローバルキューに非同期で渡された場合などでは、関数の終了後もクロージャGが実行される可能性があります。
次に、コード例を挙げて説明します。例えば:
func someFunction() {
let f = {
print("Hello, World!")
}
DispatchQueue.global().async {
f()
}
}
この場合、f
がグローバルキューに非同期で渡されるため、関数が終了してもクロージャは実行されます。これにより、Escapingクロージャとして扱う必要が出てきます。また、セマフォを使った例を考えてみます:
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.global().async {
// ここで非同期処理を行う
semaphore.signal() // 処理が完了したらシグナルを送信
}
// メインスレッドで待機
semaphore.wait()
print("処理完了")
このように非同期処理を行い、処理が完了したことをシグナルで通知します。そして、メインスレッドで待機し、全ての処理が完了したら次のステップに進むことができます。
クロージャを適切に管理し、メモリリークを防ぐためには、ウィーク参照やアンオウンド参照をしっかり理解し、Escapingクロージャとして定義するかどうかを慎重に判断する必要があります。これで、Swiftでのクロージャの使用についての基本を押さえることができました。 なので、エスケープが大事だなという話がありました。最近出たコンカレンシーについて少し触れましょう。withSettingAction
をコンカレンシーでasync
関数にして、その中でタスクを実行すればいいのかという質問があります。
ここでフォールバックをコンカレンシーに変換する関数、例えばcontinuation
を使います。この中でThread.sleep
とsignal
関数を使用して、今までDispatch
で処理していた部分を渡してあげます。この方法で終了した場合、continuation
で終了通知をする仕組みです。
async
/await
を使ってコンカレンシーに書き換えると、エスケーピングを気にする必要がなくなるのです。これは、コンカレンシーの場合にはスレッドをまたいでクロージャが延命される可能性があるためです。
従来の考え方では、いつクロージャが呼ばれるかわからないためエスケーピングを利用するのが常識とされていました。しかし、コンカレンシーではこれは必要ないのです。withCheckedContinuation
等が利用される場合、実際の動作が違うのです。
例えば、クロージャを受け取るasync
関数があったとして、その中で関数f
を呼ぶとします。async
関数の特徴として、ある非同期関数をawait
で実行したとき、処理が完全に終わるまで待つのです。この間、中断しています。処理が終われば次に進むという流れです。
結果的に、非同期関数が順番通りに動くことが確認できます。1秒待ってプリントし、続いて処理が進むため、スレッドを渡そうがクロージャを延命する必要がないのです。
このように、async
/await
を使うことでエスケープは不要であり、順番通りに確実に動作することがわかります。ただし、別の形で延命する必要がある場合も考えられるため、状況に応じて使い分ける必要があります。
まとめると、async
/await
の利用により、スレッドの中断やエスケーピングが不要になる点が非常に興味深く、面白いと感じました。
これで今日の勉強会は終わりにします。お疲れ様でした。ありがとうございました。