うるおいらんど

アプリ開発やサイト制作のメモとか。

【Swift 3】関数内で非同期処理を行った後の値を返り値としたかった話【クロージャ】

魚ライン
魚ライン

どもども。Reoです。

もうずっと前から理解不能だったヤツをようやく理解できた気がするのでメモがてら書いていきます。

クロージャが使える方からすれば、ど定番の定石ってヤツなのかもしれないです。

 

今回は「とある関数内で非同期処理を行った後の値を返り値としたい場合」にどうすれば良いのかという話をしていきます。

 

例えば以下のコードを実行してみます。(Playground上で動かしています)

 

このgetCat()関数では、返り値に”にゃんこ”が欲しくて実行しますが、実際に返される値は”初期値”のままです。

 

この場合、非同期で行なっているdispatchQueue.async {  }ブロックの処理完了を待たずに返り値が返されてしまいます。

 

解決策は「クロージャを引数に使う!」という方法です。

 

結構前からこの問題に悩んでいて、何やらクロージャというものを使えばどうにかなるらしいけれど、そもそもクロージャってなんなの?って理解してませんでした。

クロージャの説明を調べて見てもなんかよくわかんないし、とりあえず以前までは返り値を扱うこと自体を諦めていました。そうしてその場しのぎコードが生まれていたんですが・・・。

最近その辺をちゃんと勉強しまして(前回の記事「「詳解Swift(第3版)」を読破したぞいっ」より)、クロージャってなんなのかようやくちょっと理解しました。

 

 

以下がクロージャを用い、先ほどの問題を解決したコードになります。

先ほどと同様に”にゃんこ”を得ることを目的とした関数になります。

 

これがまた分かりづらい〜〜〜んですよね。

まじでクロージャを理解する前(詳解Swiftを読む前)だと意味がわかりませんでした。

 

ざっくりとクロージャの説明

超ざっくりとしたクロージャの説明ですが、基本的には名前のない関数みたいなもんです。

引数と返り値を持つことができます。

例えば簡単なクロージャだと

() -> ()の部分は (引数) -> (返り値)を指定します。

上記の例に引数を設定してみると、

返り値のみを設定すると、

 

引数と返り値両方を設定すると

 

超簡単な例だと上記のような感じです。

その他にもクロージャには値をキャプチャしたり省略した書き方ができたりと、とにかく奥が深く説明すると長くなってしまうので、超簡単に説明させていただきました。

以下の記事も参考にすると良いかもしれないです〜

【Swift】クロージャの使い方。名前の無い関数を作る。(Swift 2.1、XCode 7.2、Android:無、iOS:有、興奮度:C)


 

 

非同期処理後の値を扱おう!

そして本題です。

 

先ほどの2つの関数を横並びで比較してみます。



まず第一の特徴として、関数の引数と返り値が異なります。

想定しているのはgetCat()を実行すると”にゃんこ”という文字列が得られるという挙動なので、非同期処理がなければ左側のgetCat()->String関数でおkということになります。

 

この左側の返り値の型をそのまま右側の引数のクロージャの引数にしています。

イメージ的には、getCat()を実行した返り値がクロージャの引数に返ってきたみたいな。

 

クロージャの型は (String) -> () です。Stringを引数に返り値はありません。

関数の中では、以下のように呼び出すことができます。

引数ラベルの名前は適当につけました。

 

気になるのはこの@escapingですよね。

これは、クロージャを関数の引数とする場合に必要となることがある修飾詞となります。

おもに「@escapingは関数の呼び出しが終了した後でもクロージャが使い続ける可能性がある場合に必要」となります。

まだ理解が浅いのでなんとも言えないんですが、コンパイラに怒られちゃうからつけてるっていうのが正直なところ。

 

このクロージャを非同期処理の最後に呼び出しています。

引数に本来返り値としたかった値を指定します。

関数内に、非同期処理のブロックの後に何か処理があれば、そちらも並列して実行されています。

 

あとはこの関数の呼び出し方です。

もし、関数の引数ラベルがあった場合は

といった書き方になります。

 

ちょっと省略した書き方になっていますが、丁寧に書くとこんな感じ

 

getCatの引数にクロージャを指定します。さらにクロージャを分けて書くと分かりやすいかもしれない。

4通りの書き方を書きましたが、どれでもおkです。

 

この書き方をしてようやく@escapingの「関数の呼び出しが終了した後でもクロージャが使い続ける可能性がある場合」というのを理解したのでちょっとだけ脱線。

 

クロージャは呼び出されるまで実行されないので、引数に設定した時点では評価されません。

なのでまずは普通に関数内部を上から順に実行します。

getCat({ str in   print(“getCat:\(str)”) })と書くと正直簡潔だけど慣れるまで本当わかりづらい・・・。

関数内部でクロージャを呼び出す部分に到着してようやくクロージャが呼び出されます。

 

これをうまく利用して、想定した返り値をゲットするというわけです。

説明がむずい!!!

 

ちなみにこのクロージャが呼ばれるタイミングは結構差があります。

流れをざっくり書いてみましたが、なんか余計わかりづらい気もする・・・



 

 

下のログを見ていただくとわかるんですが、getCat()の関数の処理が終わって、さらにその後に続くfor-in文の途中でようやく非同期処理が完了して、クロージャ内のログが表示されています。

何度か実行しなおしてみると、呼ばれるタイミングが違うときがあるんですね。

for-in文までたどり着く前にクロージャが呼ばれることもありますし、for-in文の途中で呼ばれることもあります。

 

色々頑張って図を作ったわりに、ログと流れの番号がグダグダだったりするのは大目に見ていただきたい・・・・。

 

なんだか空回った説明になってしまった気もしますが、この辺で。

GCD周りにはわりと毎回苦しめられるんですが、クロージャをうまく使いこなすことでどうにか慣れていきたいです。

 

脱初心者を目指していますが、至らぬ点もいっぱいあると思いますので、何か間違っているよーとかこうしたほうがいいよーとかあればぜひ教えていただければ嬉しいです。

 

参考

【swift】非同期処理後のクロージャの処理が遅いとき

Swift: Return boolean in GCD Completion Block Ask Question – stackoverflow

[Swift 3] Swift 3時代のGCDの基本的な使い方

Swift3のGCD周りのまとめ – Qiita

詳解Swift 第3版 Programming Language Swift Definitive Guide

 

 

少しはスキルアップしたと思いたいところだけれど、どこかで循環参照してるよ!とかにはまだなかなか気づかないし怖いよー

返り値はないって書いてるけど実際はVoidが返ってるよとか言われたらもうゴメンナサイって感じだよー

 

実際はこれAlamofireを扱う時に使いました。気づかないうちにクロージャに入ってて気づかないうちに非同期処理になってたなんてつい最近まで知らなかったよ><

魚ライン
モッピー!お金がたまるポイントサイト
魚ライン

Swiftの記事一覧を見る

コメント

コメントは認証制です。詳しくは下記の注意をお読みください。

コメントを残す

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

魚ライン 魚ライン