Skip to content

Swift Concurrencyで非同期関数を実装する

前のページでは、ファイルを探し、内容を読み、単語数を数え、その結果を保存する処理をGCDと完了ハンドラで実装しました。

続いて、同じ関数をSwift Concurrencyのasync/awaitを用いて書き直します。

非同期関数には、関数宣言の引数リストの後ろにasyncを付けます。また、Errorを発生させることも可能であるため、throwsも付けます。

func loadText(from url: URL) async throws -> String

前ページで作った三つの非同期関数を、全てSwift Concurrencyで再実装します。

enum WordCountError: Error {
case fileNotFound
case cannotReadFile
case cannotSaveFile
}
func fetchTextFileURL(for id: String) async throws -> URL {
// ファイルの取得に時間がかかる状況を再現する
try await Task.sleep(nanoseconds: 1_000_000_000)
guard let url = Bundle.main.url(
forResource: id,
withExtension: "txt"
) else {
throw WordCountError.fileNotFound
}
return url
}
func loadText(from url: URL) async throws -> String {
do {
return try await Task.detached {
try String(
contentsOf: url,
encoding: .utf8
)
}.value
} catch {
throw WordCountError.cannotReadFile
}
}
func saveWordCount(_ count: Int, to url: URL) async throws {
let report = "単語数: \(count)"
do {
try await Task.detached {
try report.write(
to: url,
atomically: true,
encoding: .utf8
)
}.value
} catch {
throw WordCountError.cannotSaveFile
}
}

三つの関数はいずれも、処理結果をreturnし、失敗した場合はエラーをthrowしています。

前ページの実装では、非同期関数の処理が完了したことを、完了ハンドラを用いてコード上で明示的に通知していました。async関数では、この明示的な通知が不要となり、通常の関数と同じように処理結果をreturnし、問題が起きたらエラーをthrowできます。

三つの非同期関数を再実装したので、処理全体は次のように再実装します。

func createWordCountReport(
for id: String,
to outputURL: URL
) async throws {
let inputURL = try await fetchTextFileURL(for: id)
let text = try await loadText(from: inputURL)
let count = countWords(in: text)
try await saveWordCount(count, to: outputURL)
}

コードの可読性が上がったことは一目でわかると思います。 async/awaitを使った実装では、実際に処理が進む順番と同じように、ネストせずに、処理を行う関数を上から下へ並べることができています。 また、Errorを直接扱うことができるため、内部の非同期関数でErrorが発生した場合もそれを直接、createWordCountReportの呼び出しもとにそのまま伝えることができます。前ページの実装のように、一つ一つエラーが発生した際にそれをそのまま呼び出しもとに通知するコードを書く必要もなくなります。

guard let inputURL = inputURL else {
completion(false)
return
}

結果として、全ページの実装が抱えていた問題は全て解決され、さらに、処理内容は全く同じですが、コード行数は25行から9行へと減りました。