筆者がSwiftを勉強しだした頃に、コールバックをよく「電話したら向こうからコールバックされるのと一緒だよ」と説明されるのですが、筆者はこの言い回しがピンときませんでした。ここではクロージャによるコールバック処理に特化して筆者なりに分かりやすく説明をしたいと思います。
#サンプルコード
早速ですが「コールバックとはなんぞや?」を理解するために、具体的なサンプルとして0から9までの数字をランダムに表示するゲームを考えてみましょう。
下記ではgame.start()
にてメソッドをコール後、completion(score)
にて呼び出し元の引数であるメソッドを呼んで(コールバックして)います。
class Game {
var score = 0
func start(completion: (Int) -> Void) { // 呼び出し先
score = arc4random() % 10
completion(score) // ここでコールバックする(呼び出し元に処理を戻す)
}
}
let game = Game()
game.start(completion: { score in // get.start()でコールする:呼び出し元
print("Result is \(score)")
})
ここで言うfunc start(completion:)
のcompletion
が「コールバック関数」と呼ばれます。「コールバックされた時に実行したい処理(関数)を書く引数だよ」ってことですね。
Swiftではcompletion
という引数名がよく出てくるのでサンプルもそうしましたが、他にもcallback:
, onSuccess:
, onError
といった引数名が、コールバック関数名としてよく使われます。
#クロージャによるコールバックのメリット
まずクロージャによるコールバックが良いのは「呼び出し元が、呼び出し先で処理された返り値を使って、本来やりたかった処理をシームレスに実行できる点にある」というのが筆者の理解です。
##①余計な変数を増やさなくて済む
もしコールバックの形式ではないメソッドにすると以下のようになります。
class Game { // コールバックを使わない場合
var score = 0
func start() -> Int {
score = arc4random() % 10
return score
}
}
let game = Game()
var resultScore = game.start() // 新しく増えた
print("Result is \(resultScore)")
どうでしょうか?これでは余計な変数resultScore
が増えてしまうことになります。変数は増えれば増えるほど後々にバグの温床になったり、「このresultScore
ってこの時はどんな値だっけ?」と考えることが増えるため開発効率の低下にも繋がります。
##②可読性が高くなる
また「呼び出し元(メソッドの実行箇所)のすぐ後ろにバックされた時の処理を記述できるため、処理の流れを追いやすいという可読性の良さも大きなメリットだと感じています。
上記の例では、コールバックを使わない場合もすぐあとにprint()
が記述されているので良いですが、もしこのコードを「Result is X in 2019.MM.DD」のように今日の日付も合わせて出力したいとなったとします。するとgame.start()
とprint()
の間に「今日の日付を取得し変数に入れる」といった処理を書いてしまいがちで可読性が低いコードになってしまうんですね。
#そもそもなぜコールバックを使う必要があるのか
なぜコールバックのような処理流れや書き方が必要になるのかを理解するためには、まず今のプログラミングの基本設計として「この処理を誰がやるのか」というのを常に考えた設計がなされていることを知らねばなりません。
例えばiOSで言うと、ViewControllerやViewModelといった登場人物(正確にはXcodeが内部でこれらの型をもとに自動生成したインスタンスたち)が出てきますが、「この処理はViewの描画を担当するViewController(の中のメソッド)でやるべきだよね」、「逆にこっちは内部的な計算処理だから裏側の処理を担当するViewModel(の中のメソッド)でやるべきだね」というように仕事の分担をさせるんですね。
その時に「ViewControllerからViewModelのメソッドを呼び出した(コール)後、ViewModel側のメソッドで処理された結果を戻して(バック)もらって、その結果を元に別の処理を実行したい」といったケースが多々あります。
その時にこのコールバックを使って書くと良い感じに書けるんですね。より具体的な例を出そうとすると書き切れないので、皆さんも早く現場の実開発に入って分からなくなったらまたここに戻って復習してを繰り返し、身体に覚え込ませてください。
知識は必要になった時に初めて身に付くものです👍
#おまけ: game.start(completion:)部分の別の書き方
コールバックに対する理解や使い方の説明は上記で終わりですが、一般的に上記のようなクロージャのコールバックを書く場合はトレーリングクロージャと呼ばれる書き方を使って書きます。
/*本来はメソッドの実行なのでこういう形*/
game.start(completion: { score in
print("Result is \(score)")
})
/*トレーリングクロージャ(メソッドの最後の引数がクロージャの場合にクロージャを()の外に書くことができる)を使って*/
game.start() { score in
print("Result is \(score)")
}
/*メソッドの引数にクロージャーが1つしか定義されていない場合は()自体も省略することができ*/
game.start { score in
print("Result is \(score)")
}