はじめに
本記事はWWDC2021にて新しく追加されたAPIや新OSの便利な使い方を紹介する連載記事の第2回となります。前回はWWDCの概要について説明しました。
第2回ではWWDCの発表内容まで踏み込んで、プラットフォームに限定されず広く使われていくであろうアップデートについてご紹介します。特にSwift Concurrencyの発表に関するものをまとめています。
Swift Concurrencyを使ったAPIは既に公式のフレームワークからいくつも提供されているため、今後の開発で目にすることが増えてくるかもしれません。いざという時に驚かないために、まだWWDCをご覧になられてない方はこの記事で予行練習するのはいかがでしょうか。
非同期処理を分かりやすく安全に! Swift Concurrencyの登場
このセクションではSwift Concurrencyの登場によって使えるようになったasync/await構文やActorについて紹介します。async/await構文を使うと、従来の非同期処理のコードを分かりやすく、かつより堅牢にすることができます。さらに、Actorと呼ばれる新しい型を介することによってデータ競合の発生を防ぐことができます。そのため、並行処理中に安心して可変変数を扱うことができるようになります。
非同期処理実装における課題とasync/await構文
まずはじめに、従来の非同期処理実装における課題とasync/await構文を使った解決方法を説明します。
下記のコードは非同期処理を行う関数の仮引数にクロージャを定義し、非同期処理が完了した際にそのクロージャを実行するという実装方法です(以降、この実装および関数の仮引数にあるクロージャをCompletion Handlerと呼びます)。Completion Handlerは標準ライブラリに含まれるURLSessionやUIAlertActionなどの多くの型にて用いられていることからも分かるように、今まで一般的に用いられてきた実装方法です。そのため、Completion Handlerを従来の実装の例として扱います。
func fetch(completion: @escaping (Data?, Error?) -> Void) { // ① URLリクエストを準備する let request = URLRequest(url: URL(string: "https://twitter.com/k_koheyi")!) let task = URLSession.shared.dataTask(with: request) { data, response, error in // ③ URLリクエストの完了を通知する if let error = error { completion(nil, error) } else if (response as? HTTPURLResponse)?.statusCode != 200 { completion(nil, NetworkError.failed) } completion(data, nil) } // ② URLリクエストを実行する task.resume() }
従来の実装、Completion Handlerの課題
Completion Handlerには次の3つの問題点があります。
- 構造化プログラミングに反しており、処理がジャンプする
- Completion Handlerが正しく呼ばれない可能性がある
- Swiftのtry-catch文を使った直感的なエラーハンドリングができない
まず、「構造化プログラミングに反しており、処理がジャンプする」問題について説明します。構造化プログラミングとは、ifやforなどの制御文を用いるプログラミングのことです。構造化プログラミングによって、goto文を使った処理のジャンプを廃止することができ、プログラムを上から下へ順に見ることにより処理を自然に追えるようになりました。一方で、上記のプログラムは上から下の順に実行されません。下記のサンプルコードは上記のサンプルコード処理の実行順をコードにコメントしたものです。処理は①から③の順に進みますが、プログラムを上から順に読むと②より③が先に現れることが分かります。これが構造化プログラミングに反している例であり、歴史的に見てもコードを分かりづらくする要因となっています。
// ① URLリクエストを準備する .... let task = URLSession.shared.dataTask(with: request) { data, response, error in // ③ URLリクエストの完了を通知する .... } // ② URLリクエストを実行する task.resume()
続いて、「Completion Handlerが正しく呼ばれない可能性がある」問題について説明します。Completion Handlerを使う場合、Completion Handlerの引数に非同期処理の結果を渡し、非同期処理から呼び出し元の処理に戻ります。処理の流れを見ると、Completion Handlerの呼び出しと、ある関数からreturn文を使って値を返す処理は、結果を返すという点では似ています。しかし、関数の場合はreturn arrow ->を使って戻り値の型を定義したら関数のスコープを抜けるまでに値をreturnしないとコンパイルが通らないのに対し、Completion Handlerは呼び出さなくてもコンパイルが通ります。そのため、Completion Handlerを適切に呼ぶことはプログラマーの責任であり、ミスが出やすい部分となります。Completion Handlerを呼ぶのを忘れてしまったり、間違えて複数回呼んでしまう誤った実装をしたりしたことがある人は多くいらっしゃるのではないでしょうか。
最後に、「Swiftのtry catch文を使った直感的なエラーハンドリングができない」問題について説明します。Completion Handlerでは、非同期処理中にエラーが発生すると、そのエラーをCompletion Handlerの引数に渡します。従って、引数から渡されたエラーがnilか否かによってエラーの発生を検知する必要があります。下記にエラーハンドリングがどのように異なるかを示します。Completion Handlerを使わない実装の方が直感的であることに加え、エラーが発生した際に自動でtryしているスコープから抜けるので便利です。
// Completion Handlerを使わない場合(同期処理の場合) do { let result = try fetch() print(result) } catch { print(error) } // Completion Handlerを使う場合(非同期処理の場合) fetch { result, error in if let result = result { print(result) } if let error = error { print(error) } }
async/await構文による解決
async/await構文を使用したサンプルコードを下記に示します。これは先述したCompletion Handlerを使ったコードをasync/await構文を使って書き直したコードであり、同じ出力を得られます。async/await構文を使って非同期関数を作る際には、戻り値の型を示すreturn arrow ->の前にasyncと書きます。そして非同期関数はawaitを使って呼び出すことができます。
下記の例ではHTTP通信を行うURLSession.shared.data(for: request)の前にawaitが付いていることが分かります。なお、このURLSession.shared.data(for: request)は新しく追加されたasync/await構文用のインタフェースであり、他のCompletion Handlerを使用していたAPIにも同様のインタフェースが追加されています。このように非同期関数を呼び出すと、HTTP通信は実行中のスレッドを止めずに実行され、通信が完了すると関数内の処理を再開し変数dataを戻り値として返します。
// Completion Handlerを使った実装 func fetch(completion: @escaping (Data?, Error?) -> Void) { let request = URLRequest(url: URL(string: "https://twitter.com/k_koheyi")!) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(nil, error) } else if (response as? HTTPURLResponse)?.statusCode != 200 { completion(nil, NetworkError.failed) } completion(data, nil) } task.resume() } fetch { data, error in ... } // async/await構文を使った実装 func fetch() async throws -> Data { // ① URLリクエストを準備する let request = URLRequest(url: URL(string: "https://twitter.com/k_koheyi")!) // ② URLリクエストを実行する let (data, response) = try await URLSession.shared.data(for: request) if (response as? HTTPURLResponse)?.statusCode != 200 { throw NetworkError.failed } // ③ URLリクエストの結果を返却する return data } do { let result = try await fetch() } catch { ... }
このように記述すると次のように挙動します。
- 非同期処理をまるで同期処理のように上から下に順に書いていける
- 非同期処理の結果は戻り値で返すように関数定義している
- asyncと同時にthrowsも利用できる
そのため、先述した課題を解決できて、直感的なコードになっていることが分かります。