https://www.youtube.com/watch?v=HpYo-6ZxDDQ
今回は A Swift Tour
の「関数とクロージャー」の中から、前回に引き続いてクロージャーについて。今回はそのさまざまな表記方法を中心にクロージャーの基本的なところを眺めていく回になりそうです。クロージャーはさりげなくさまざまなところで多用する機能になっているので、この機会にしっかり親しんでおけたらいいなと思ってます。どうぞよろしくお願いしますね。
——————————————————————————— 熊谷さんのやさしい Swift 勉強会 #43
00:00 開始 00:55 クロージャーの定義 01:25 匿名関数 03:04 コールバック関数 05:39 コンプリーションハンドラー 06:48 エスケーピング・クロージャー 09:19 クロージャーをプロパティーで保持 12:18 関数型 12:49 クロージャーではなく関数を渡す 14:40 クロージャーの特徴 15:34 クロージャーの省略表記 17:52 トレーリングクロージャー 19:35 戻り値の型の省略 19:57 型推論を用いた省略表記 21:40 引数の型の省略 22:06 引数リスト表記の省略 22:38 引数名の省略 23:20 引数を使うかどうかの明記 24:54 省略表記の活用例 25:21 省略表記から全て表記する形に書き換えてみる 26:45 クロージャーに慣れることが上達のポイント 28:59 関数を実行している感 31:38 質疑応答 34:57 タプルスプラット 41:02 引数の片方だけ省略はできない 41:50 エスケーピングクロージャー 44:34 クロージングオーバー 46:09 キャプチャーリスト 46:44 クロージャーはスコープを越える 48:42 クロージャーを引数で受け取る 51:14 戻り値として返したクロージャーの扱い 51:43 次回の展望 ———————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #43
はい、では始めますね。今日はクロージャーの話をします。クロージャーの話が終わると、次はオブジェクト指向の話に移ります。でも、クロージャーはもう少しで終わるとはいえ、かなり重要な機能なので、前回もクロージャーの話をしていました。しかし、あまりうまく説明できなかった部分もあるので、今日の1時間を使ってクロージャーを改めてじっくりと見ていきたいと思います。
まあ、その予定ではありますが、早く終わればオブジェクト指向の方に移るかもしれません。まずはクロージャーの定義についてお話しします。これは前回の最後の方の時間で見たスライドなので、覚えている方も多いかと思います。このスライドをベースに、いろいろお話を進めていきます。
まずクロージャーとは何かという自分の中のイメージを整理します。特命関数と呼ぶ言語もあるかな、という印象があります。要は普通の関数は名前付きのものですが、その名前のないバージョンがクロージャーです。名前のないものは即席で使うことが多い、という特徴がありますね。
パール言語では、特命関数のほかに特命ハッシュや特命配列といったものもあります。要は、名前を付けずに即席で作る、変数に代入せずにそのまま使う感覚です。ハッシュと配列については関数と似たような感じですけど、即席で使うこともありますし、どこかのオブジェクトのプロパティに保存しておいて、あるタイミングで使うという風に使われることもあります。
よくある例としてはコールバックです。例えば、クラスがあり、そのクラスにリクエストを送ってレスポンスをもらう関数があるとします。レスポンスが Int
型だとしましょう。リクエストを投げてレスポンスを返す関数ですが、例えば7行目に非常に重たい処理があったときに、非同期的にその処理を別スレッドに投げておいて結果がもらえたらリターンするという感じにしたいとします。
この場合、DispatchQueue
をインポートしておく必要がありますね。例えば、import Dispatch
として、DispatchQueue.global().async
で処理を投げるとします。これだと同期的な処理になってしまうので、重たい処理をするとブロックして待たせてしまうという問題が起こります。
このようなときに使われるのがクロージャーを用いたコールバックです。例えば、コンプリートハンドラー(completionHandler
)を取る関数にします。以下のようなコードで示されます。
func fetchData(completionHandler: @escaping (Int) -> Void) {
DispatchQueue.global().async {
// 重い処理
let response = heavyTask()
completionHandler(response)
}
}
このように、処理が終わったタイミングでcompletionHandler
を呼び出し、その引数としてレスポンスを渡します。@escaping
キーワードを付けないと別のスレッドに渡せないので注意が必要です。これで OK ですね。 とりあえず、クロージャーが活用されている場面について説明します。呼び出すときは、例えば何らかのオブジェクトを生成し、これに対してリクエストを行い、そのリクエストに対してコンプリーションハンドラーとしてクロージャーを渡します。このとき、レスポンスを受け取って何も返さないクロージャーとして実装します。この中でレスポンスに対して何かを行い、コードを書いていくという流れです。
例えば、このクロージャーを渡す際に、以下のようなコードが考えられます。
request(completion: { response in
// レスポンスに対して何かをする
})
このクロージャーは、リクエストが終わってレスポンスが得られたときに呼び出されます。具体的には、21行目から23行目までのコードブロックを後で実行するような形です。これがクロージャーを活用する典型的な例の一つです。
他にも、あらかじめプロパティとしてコンプリーションハンドラーを持たせることも可能です。これで、レスポンスを受け取って何も返さないクロージャーを用いる場合の一例を示します。
class SomeClass {
var completionHandler: ((Response) -> Void)?
init(completionHandler: @escaping (Response) -> Void) {
self.completionHandler = completionHandler
}
func performRequest() {
// リクエスト処理
completionHandler?(response)
}
}
このようにして、リクエストに対してコンプリーションハンドラーを渡さなくても、あらかじめ初期化と同時に設定しておくことができるようになります。
関数もクロージャーの一種です。例えば、以下のように関数を使ってリクエストの処理を定義することができます。
func handleResponse(response: Response) {
// レスポンスに対して何かをする
}
request(completion: handleResponse)
即席の名前なし関数(クロージャー)を渡さずに、あらかじめ名前が付いている関数を渡すこともできます。これにより、コードの見通しがよくなる場合もあります。
クロージャーの特徴として、「クロージャー」に対する省略表記が用意されています。例えば、トレーニングクロージャーの記法では、以下のように丸カッコの外にクロージャーを出すことができます。
request { response in
// レスポンスに対して何かをする
}
このように、クロージャーの書き方にはさまざまなバリエーションがあり、状況に応じて最適な表現方法を選ぶことができます。
次回は、クロージャーの詳細な書き方やクロージングオーバーの仕組みについて詳しく見ていきましょう。 なので、こういう書き方もできたかな。ちょっとやってみましょう。丸カッコの外に出して、丸カッコの外に出したときにはラベル名がいらなくなる、つまり省略することになり、クロージャーの最後の丸カッコも外に出す都合でいらなくなるという書き方です。これは通るのか? 通るんだ。丸カッコなしでも書けるのね。そうすると、この丸カッコがいかにも冗長ですよね。引数が何もない場合は引数リストが省略できるというのも、クロージャーの省略記法の一つです。さらに、戻り値が Void
だった場合は省略できるよ、というのは普通の関数と同じですね。省略ができて、これも問題なく動いていますね。
さらに、今回の場合は型推論も効いてきて、そうするとさらにクロージャーの省略記法ができるようになります。型推論が効くというのは、このリクエストの引数がレスポンスを引数に取り、戻り値が Void
を返すというクロージャーになっているので、関数型になっているので、今回の場合は Void
だったから省略していますけど、これがもし戻り値が Int
だったとすると、基本的には Void
ではないのでリターン記法の省略ができないわけですけれども、型推論も伴ってくると、ちゃんと記述することが求められます。
例えば、戻り値が Int
だったとすると、戻り値を何かしらで使わないといけない。普通だと戻り値が Int
型っていうのは省略できないところですが、型推論のおかげで戻り値は Int
と決まっているわけだから省略が許される場合もあります。また、引数の型もレスポンス型と決まっているので省略することができるということです。
このように、引数の型も当たり前なので省略できるよと。さらに、引数リストや戻り値が省略されている状況だと、この書かれている変数名が引数リストだよねっていうことが明らかなので、この丸カッコも省略してよくなる、こういう省略方法があるのです。
あと、大事なポイントとして、クロージャーの大きな特徴として、引数リスト自体も省略可能ですよというのがあります。こうやって丸っと省略できるけれど、こうすると引数リストや戻り値の型、それに実際の実装のボディ部分を隔てる必要がなくなるので、in
という隔てるためのキーワードまで省略できるようになります。ただ、今回の場合は、エラーが出ているのが見受けられるかと思いますが、これは省略すると大事なポイントとして、最初の引数から順に $0
、$1
、複数取る場合はこういった名前で変数を使っていくことができる、匿名の変数みたいな感じですね。
Perl 言語を使っている人はどれぐらい居るか分かりませんが、Perl 言語と同じような省略記法になっています。シェルスクリプトとかもこういう雰囲気ですよね。パラメータを引数リストの記述を省略して、中でその省略記法を使ったときに限ってはコンパイルが通る仕様になっているのです。なので、何らかの方法でこれを使ったとして、こうすることでコンパイルも通ります。これが一番短い表記の方法ですね。
なので、よく使われるのは本当に即席で使うような場合です。例えば、マップに対してそれを2倍したものを返す、みたいな場合です。これが便利な表現になってくるのです。
この一番下の答えを省略記法をやめていくと、
element in element * 2
まず、これが最初になりますね。他にエレメントを引数リストとして明示的に書く方法もあります。さらに、戻り値が Int
ですよ、という書き方もあります。エレメント自体が今回 Int
の配列なので Int
型ですよ、という書き方もあります。
これがトレーリングクロージャーですね。トレーリングクロージャーを使わずに書く方法もあり、これが一番長い書き方かなと思います。こういった感じです。全然見やすさが違ってくるので、省略記法があることによって使いやすさが全然変わってきます。ただ、その省略記法だけでは情報が全然足りないときとか、そういったときにはどうしてもこういう長い書き方を採用せざるを得ないです。
このように、クロージャーの特徴を理解し、適切な記法を選ぶことで、コードがより読みやすく、保守しやすくなります。 クロージャーは、Swiftの中では非常に柔軟な書き方ができる点が大きな特徴です。しかし、それ故に初心者にとっては難しい部分でもあります。実際に私もこれに苦労しました。これに慣れるまでは時間がかかりました。
最初のうちは、クロージャーの表記に徹底的に慣れていくことが、ステップアップにつながると思います。ステップアップできるというのは、単純に読む力が向上するだけではなく、クロージャーを使って関数に関数を渡すことが心理的に簡単にできるようになってくるからです。そうすることで、Swiftの関数型プログラミングのパラダイムの一つである「高階関数」が身近になります。
高階関数は、非常に難しい部類に入りますが、詳しい人はとことん詳しく、それが好きな人が突き詰めていく領域です。それに対し、Swift言語自体は高階関数を前提とした言語ではなく、関数型言語でもありません。しかし、この程度のことを把握していれば、Swiftの表現力が高まります。この辺りをまず慣れておくことが、Swiftをうまく使えるようになるための良いステップだと感じています。
コメントが面白いですね。例えば、リクエストの後に丸カッコを書くと、トレーニングクロージャーのように関数を実行している感じが出ていいかもしれません。パラメータがあれば自然とそうなることもありますが、丸カッコを添えると関数を呼ぶという重要な構文になります。
具体例として、19行目から23行目に「リクエスト」という場面を見てみると、確かにそう感じます。このマップに丸カッコをつけるとどうなるでしょうか。別の見方もありますけど、マップという一般的な概念が邪魔をしているのかもしれません。少しまどろっこしくも見えるような気もしますが、どちらを選んでも正解です。もちろん、26行目のほうが分かりやすいと言う人もいれば、27行目のほうが分かりやすいという人もいるでしょう。
どちらを書いても誤解を招くわけではないので、その場に応じてどちらの表記が分かりやすいかを考えることが重要です。丸カッコを省略するのが一般的に馴染んでいることもあるので、どちらでもいいのです。ここは書こう、ここは省略しようと判断できるのは良いことだと思います。
最後に、トレーニングクロージャーについても色々と言いたいことがありますが、その前にエスケーピングクロージャーのポイントを抑えておくことも重要です。これについても紹介しておきたいと思います。 その前に寄せられている感想をもう少し見てみましょう。「使わないから省略する」のではなく、「省略していることを明示して初めて省略できる」というのは難しいですね。これ、補足してもいいですか?
はい、お願いします。ちょっとiPadの音が悪いかもしれないですけれども、「ダラーゼロ」の話のところがありましたよね?
はい、ありましたね。
そこで、$0
を使うと初めてin
が省略できるという話があったと思うんですけど、そうですね。
$0
を使わないで頑張って省略しようとすると、「ワイルドカードin」みたいな感じが一番短くなると思うんですけれども。
はい、そうですね。「ワイルドカードin」で。
そうそう。この2つを比べたときに、ワイルドカードを使うことで「この引数は使わないよ」ということを明示していますよね。ワイルドカードを使うと。
そうなることで初めて省略できるというか。逆に$0
を使うよってして、「省略できる」というところがあったと思うんですけど。
そうですね。$0
を使わないで頑張って省略しようとすると、「ワイルドカードin」みたいな感じが一番短くなると思うんですけど。
はい、そうですね。「ワイルドカードin」で。
で、逆に$0
で使うよとしたら、それでも省略できるというところがありましたよね。
そうですね。もっと省略ができるようになってきます。コードを見たときに$0
って入っていれば「あ、1個目の引数なんだ」とわかりますから。
そうですね。この状態で$0
を使わないでコンパイルエラーが出るのは、結構Swiftらしさじゃないですか?
そうですね。
これは読みやすさという配慮もあるでしょうし、コードを書いている人にとっても、クロージャーに引数が渡ってきていて、それを使わないのかという問いかけにもなりますよね。使わないよ、と表現したり、使うよ、と表現したり。
そうです。これによってパラメータが例えば3つ4つあったときに、3つまでしか使ってないよ、という感じで「使い忘れていない?」と指摘してくれるんですよね。
そうですね。面白いですね。クロージャーが結構対面している感じがしますよね。
ありがとうございます。お互いにそうですね。
このアンダースコアの使い方は面白いですね。そういう読み方もいいと思います。そして、もう一つせっかくだから面白いところを。混乱を招くだけかもしれませんが、コンプレションハンドラーで例えばレスポンスと合わせてストリングも取るみたいにしたときに、このときにですね、$
キャラクターの使い方でコンパイルエラーになる場合があります。
例えば、値を2つ持ったタプルのとき、$0
と$1
ですか?
そうですね、$0
と$1
はいけますね。でも$0.0
みたいにするとエラーになるんですよね。
これ、前はできた気がするんですけど、できなくなっていますね。今回本当は「これもできるんですよ」と言いたかったんですが、できなかったですね。
そうですね、残念ですが。 前回、タプルで情報を取得した件についてです。この手法で実行することが可能ですね。現在、実行中ですが、うまくいくことを期待します。
このコードだと、1つ目の引数がタプル、2つ目の引数が通常の引数になっていますよね。リクエストのクロージャーの定義自体をタプルで受け取れるようにしてみましょう。例えば、以下のように書きます:
func someFunction(request: (String, Int), anotherParameter: String) {
// 実行内容
}
タプルを使うことによって、複数の値を1つの引数としてまとめて渡すことができます。そして、この方法でレスポンスをタプルとして受け取ることも可能です。引数が複数ある場合でも丸括弧を省略できるので、見やすさを確保するために適宜省略するのが良いかと思います。
例えば、以下のように省略できます:
someFunction { (response: (String, Int), anotherParameter) in
// 実行内容
}
また、クロージャー内でタプルの要素を $0
や $1
を使って省略することも可能です。しかし、見やすさの観点から、必要に応じて明示的な名前を使用するのが望ましいでしょう。両方の引数を省略することができないので、名前を使って明確にする必要があります。
トレーリングクロージャーに関する注意点ですが、複数のトレーリングクロージャーを使用する場合はそれぞれに名前を付けて明確に分ける必要があります。
次に、「エスケーピングクロージャー」について説明します。エスケーピングクロージャーとは、スコープを超えて実行される可能性があるクロージャーに付けるフラグのようなものです。例えば、以下のような状況です:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
この場合、completionHandlerが関数の外で実行される可能性があるため、@escaping
というキーワードを使ってエスケープすることを示します。
一方、ノンエスケーピングクロージャーは、スコープ内で完結して実行されるクロージャーであり、特に明示する必要はありません。例えば、以下のような例です:
func anotherFunctionWithNonEscapingClosure(closure: () -> Void) {
closure()
}
クロージャーは、その周囲のスコープにある変数や関数をそのまま使うことができます。この機能を「クロージングオーバー」と呼びます。クロージングオーバーによって、外のスコープにある変数や関数をクロージャー内で使用することができます。
さらに、キャプチャーリストというものがあり、クロージャーが変数をキャプチャーする際の動作を明示的に指定することができます。例えば、次のように書くことができます:
someFunction { [capturedVariable] in
// 実行内容
}
これによって、新しいスコープ内で変数を定義し、その変数に対して代入を行い、クロージャー内で使用することができるようになります。クロージングオーバーと動きが少し異なりますが、使い方に応じて使い分けることが重要です。 とりあえず、何も配慮しなくてもクロージングオーバーという仕組みが活かされて、内部で使えるということです。大事なポイントとして、このクロージャーは第一級の型になっているおかげで、いろんな関数にパラメーターとして渡したり、リターンとして戻したりするなど、いろいろな操作が可能となっています。
例えば、これが関数だったとして、全体がvoid
を返す関数を返す関数だとしましょう。この場合、リターンでG
と書くことができます。こうすると、クロージャーG
はF
やA
をキャプチャーしていますね。キャプチャーではなくクロージングオーバーしていると言うべきでしょう。このリターンをした際にスコープを抜けると、F
やA
が破棄されるのかどうかという状況になります。
こうした場合、破棄しないでライフタイムを延長する必要があります。これが関数のスコープよりも延命されるようにするための配慮です。生存範囲を超えて使用されるため、その範囲を明示的にするために、これはエスケーピングクロージャーだ、とマーク付けをします。戻り値として返すクロージャーもエスケーピングクロージャーです。
通常は、引数としてクロージャーを受け取り、関数内部で使用する分には特に延命する必要はありません。外側のスコープで定義されているものが、外部が破棄される前に実行されるためです。しかし、例えばディスパッチの別のスレッドに渡すような非同期操作時には、外側で使用されるため、エスケーピングクロージャーとしてマークしておかないといけません。これにより、F
の生存範囲が終了した後に実行されるようになります。ここでエスケーピングを付けて、明示的に延命を図るマーク付けが必要です。
エスケーピングクロージャーには、パラメーターとしてエスケーピングクロージャーしか渡せません。ノンエスケーピングクロージャーを渡そうとすると警告となります。延命できないものを渡そうとするとエラーになる、そのような言語の安全設計がされています。
ちょっと話が難しくなってきましたが、要は延命する可能性があるものはエスケーピングとマーク付けされるということです。そして、エスケーピングクロージャーには、そのパラメーターとしてノンエスケーピングクロージャーを渡すことはできず、ちゃんと延命されたものだけが使えるという設計です。
また、この部分については別の機会にもう少しゆっくり話したいですが、今日はここまでとしましょう。ちなみに、戻り値として返したクロージャーもエスケーピングクロージャーであり、そのままエスケーピングクロージャーとして渡せます。
以上、クロージャーに関する説明を終えます。次回は、オブジェクトとクラスの話に移りたいと思います。今日はこれで勉強会を終わりにします。お疲れさまでした。ありがとうございました。