iOSDC 2018 Japan の一昨日の @yimajo さんのセッション、 "Swiftの生みの親によるasync/await for Swiftを徹底解説し、新しい非同期処理の手法を理解する" で「 beginAsync
の引数に渡すクロージャの型が () async throws -> Void
だけれども、この throws
はない方がいいんじゃないか」という趣旨の質問をしました。その意図について説明したいと思います。
beginAsync
って何?
beginAsync
は Swift の async/await
のプロポーザルドラフトに中で提案されている関数です。プロポーザルが通れば Swift の標準ライブラリに追加されます。
beginAsync
のシグネチャは次の通りです。
func beginAsync(_ body: () async throws -> Void) rethrows -> Void
throws
な関数は throws
な関数の中でしか使えない(か do/catch
で囲まないといけない)のと同じように、 async
な関数は async
な関数の中でしか使えません。しかし、そうすると最初に非同期処理を開始する箇所はどうすれば良いでしょうか。 main
関数まで async
にしないといけなくなってしまいます。 async
で ない の中で async
な関数を開始するための関数が beginAsync
です。
たとえば、次のような download
関数を考えてみます。
func download(from url: URL) async -> Data
ダウンロードは非同期処理なので async
が付与されています。ボタンを押したらダウンロードを開始するという処理の場合、次のように書きたくなります。( throws
な関数を呼ぶのに try
の付与が必須なように、 async
な関数を呼びのには await
の付与が必須です。)
@IBAction func pressButton(_ sender: UIButton) {
self.data = await download(from: url)
}
しかし、 pressButton
は async
でないただのメソッドなので、このメソッドの中で直接 download
を呼びことはできません。そこで beginAsync
を使って次のように書きます。
@IBAction func pressButton(_ sender: UIButton) {
beginAsync {
self.data = await download(from: self.url)
}
}
beginAsync
に渡されるクロージャの型は () async throws -> Void
なので、このクロージャ式の中では async
な関数をコールできるわけです。
何が問題なの?
前述の通り、 beginAsync
のシグネチャは次のようになっていますが、
func beginAsync(_ body: () async throws -> Void) rethrows -> Void
僕は、↓であるべきだと考えています。
func beginAsync(_ body: () async -> Void) -> Void
一見、 throws
が付与されているから非同期エラー処理ができるように見えるんですが、 そんなことはありません 。
たとえば、次のようなコードを考えてみましょう。
func foo() {
do {
beginAsync {
let data = await download(self.url)
try data.write(to: pathURL) // ここでエラーが発生したらどうなる?
}
} catch let error {
// エラー処理
}
}
もし try data.write(to: pathURL)
でエラーが発生した場合、外の catch
でエラー処理することはできるでしょうか?
この foo
が実行された場合、 beginAsync
が呼ばれると、 beginAsync
に渡されたクロージャに同期的に突入します。なので、 beginAsync
のコールが終わる前に await download(self.url)
が実行されます。
しかし、ここで await
があるので、この後の処理は非同期化されます。同じスレッドで実行される保証すらありません。そのため、 download
が呼ばれた直後に beginAsync
を抜けて、当然この時点ではエラーは発生していないので catch
節に入ることもなく、そのまま foo
を抜けることになります。
その後、ダウンロード処理が完了すると結果が data
に代入され、ようやく try data.write(to: pathURL)
が実行されます。ここでエラーが発生したとしても、もう foo
の do/catch
はとっくに抜けた後です。 beginAsync
の外側にエラーを rethrow することはできません。結果、発生したエラーは黙殺するか、クラッシュさせるしかなくなります。コンパイル時に静的に検査できない危険なコードになってしまいました。
もし、↓のように throws
がなければどうなるでしょうか?
func beginAsync(_ body: () async -> Void) -> Void
beginAsync
には throws
なクロージャを渡せなくなるので、クロージャの中で throws
な関数をコールすることもできなくなります。そのため、次のようなコードはコンパイルエラーとなります。
beginAsync {
let data = await download(self.url)
try data.write(to: pathURL) // try できないのでコンパイルエラー
}
もしエラー処理をしたければ次のように書くことになります。
beginAsync {
do {
let data = await download(self.url)
try data.write(to: pathURL) // `do/catch` の中なので OK
} catch let error {
// エラー処理
}
}
これなら beginAsync
の中でエラー処理が完結しているので OK です。コンパイルエラーによって正しくエラー処理を促すことができました。素晴らしいですね!
というわけで、 throws
要らないんじゃないかということでした。いかがでしょうか?