※この記事はSwift by Sundellの内容を日本語に翻訳したものです。
概要
Swiftの主な特徴の一つは、コンパイル時の安全性です。
これにより、開発者はより予測可能でタイムエラーが発生しにくいコードを書くことができます。
しかし、様々な要因によってエラーが発生することがあります。今回は、そのようなエラーを適切に処理する方法とそのために使用できるツールについて見ていきます。
「Handling non-optional optionals in Swift」という記事では、実際にはオプショナルではないオプショナル型の処理方法を紹介しています。この記事では、強制アンラップの代わりにguard
と組み合わせてpreconditionFailure()
を使用すること、その為の便利なAPIを提供するマイクロフレームワーク「Require」を紹介しました。
その投稿以来、多くの人がpreconditionFailure()
とassert()
の違いと、それがSwiftのthrowing
機能とどのように関連するのかについて尋ねてきました。なので、この記事ではこれらの言語の特徴とそれぞれの使い時を詳しく見ていきます。
エラー処理方法一覧
Swiftでエラーを処理する方法は次のとおりです。
-
nilを返す、またはエラー列挙型
-
エラー処理の最も簡単な形式は、エラーが発生した関数から単純にnil(または戻り値の型として列挙型Resultを使用している場合は.error型)を返すことです。これは多くの状況で非常に役立ちますが、すべてのエラー処理に使いすぎると、APIの使用がややこしくなり、欠陥のあるロジックが隠れてしまうリスクがあります。
-
throw MyError
を使用してエラーを投げる -
これには、呼び出し元がdo, try, catchパターンを使用して潜在的なエラーを処理する必要があります。または、呼び出し地点で
try?
を使用してエラーを無視することもできます。 -
assert()
およびassertionFailure()
の使用 -
特定の条件が真であることを確認します。デフォルトでは、これはデバッグビルドでfatal errorを引き起こしますが、リリースビルドでは無視されます。したがって、assertが引き起こされた場合に実行が停止することは保証されていないため、重大なランタイム警告のようなものです。
-
precondition()
とpreconditionFailure()
の使用 -
asserrtとの主な違いは、リリースビルドであっても、これらは常に評価されることです(Ounchecked最適化モードを使用してコンパイルしている場合を除く)。つまり、条件が満たされない場合、実行が続行されないことが保証されます。
-
fatalError()
の呼び出し -
これは、Xcodeで生成されたinit(coder :)の実装でUIViewControllerなどのNSCoding準拠のシステムクラスをサブクラス化するときに、おそらく見たことがあるでしょう。これを呼び出すと、プロセスが直接強制終了されます。
-
exit()
の呼び出し -
コードを使用してプロセスに存在するexit()を呼び出します。これは、コマンドラインツールやスクリプトで、グローバルスコープ(main.swiftなど)を終了する場合に非常に便利です。
エラーが回復可能か不可能か
エラー処理の方法を選択する際に考慮すべき重要なことは、発生したエラーが回復可能かどうかを判断することです。
たとえば、サーバーを呼び出していて、エラーレスポンスを受け取ったとします。それは、私たちがどれほど素晴らしいプログラマーであり、サーバーインフラがどれほど堅固であっても必ず起こることです。したがって、これらのタイプのエラーを致命的で回復不可能なものとして扱うことは、たいてい間違いです。代わりに、私たちが望んでいるのは回復して、おそらく何らかの形のエラー画面をユーザーに表示することです。
では、この場合にエラー処理の適切な方法を選択するにはどうすればよいでしょうか。上記のリストを見ると次のように、回復可能な手法と回復不可能な手法に分類できます。
回復可能
- nilを返す、またはエラー列挙型
-
throw MyError
を使用してエラーを投げる
回復不能
-
assert()
およびassertionFailure()
の使用 -
precondition()
とpreconditionFailure()
の使用 -
fatalError()
の呼び出し -
exit()
の呼び出し
以下のような場合、非同期タスクを扱っているので、次のように、戻り値nilまたはエラー列挙型がおそらく最良の選択です。
class DataLoader {
enum Result {
case success(Data)
case failure(Error?)
}
func loadData(from url: URL,
completionHandler: @escaping (Result) -> Void) {
let task = urlSession.dataTask(with: url) { data, _, error in
guard let data = data else {
completionHandler(.failure(error))
return
}
completionHandler(.success(data))
}
task.resume()
}
}
同期APIの場合、throwは最適な選択です。これは、APIユーザーに適切な方法でエラーを処理するように「強制」するためです。
class StringFormatter {
enum Error: Swift.Error {
case emptyString
}
func format(_ string: String) throws -> String {
guard !string.isEmpty else {
throw Error.emptyString
}
return string.replacingOccurences(of: "\n", with: " ")
}
}
ただし、エラーを回復できない場合があります。たとえば、アプリの起動時に構成ファイルをロードする必要があるとします。その構成ファイルが欠落していると、アプリが未定義の状態になります。 したがってこの場合、プログラムの実行を続行するよりもクラッシュする方が適切です。そのためには、より強力で回復不可能なエラー処理を使用する方が適切です。
以下のような場合、構成ファイルが欠落している場合に実行を停止するためにpreconditionFailure()
を使用します。
guard let config = FileLoader().loadFile(named: "Config.json") else {
preconditionFailure("Failed to load config file")
}
プログラマーエラーと実行エラー
重要なもう1つの違いは、エラーの原因がロジックの誤りもしくは構成の誤りなのか、またはエラーがアプリケーションのフローの正当な部分と見なされるべきかどうかです。基本的に、プログラマーがエラーを引き起こしたかどうか、または外部要因が原因であったかどうかです。
プログラマーのエラーから保護する場合、ほとんどの場合、回復不可能な手法を使用する必要があります。そうすれば、アプリ全体で異常な状況をコード化する必要がなくなり、優れた一連のテストにより、これらのタイプのエラーができるだけ早く検出されることが確認されます。
たとえば、使用する前にView Modelをバインドする必要があるViewを構築しているとします。View Modelはコードではオプショナル型になりますが、使用するたびにラップを解除する必要はありません。ただし、View Modelが何らかの理由で失われた場合でも、本番環境でアプリケーションをクラッシュさせる必要はありません。デバッグでエラーが発生しても十分です。以下は、アサートを使用する場合です。
class DetailView: UIView {
struct ViewModel {
var title: String
var subtitle: String
var action: String
}
var viewModel: ViewModel?
override func didMoveToSuperview() {
super.didMoveToSuperview()
guard let viewModel = viewModel else {
assertionFailure("No view model assigned to DetailView")
return
}
titleLabel.text = viewModel.title
subtitleLabel.text = viewModel.subtitle
actionButton.setTitle(viewModel.action, for: .normal)
}
}
assertionFailure()
はリリースビルドでサイレントにエラーが発生するため、上記のguard
ステートメントにreturn
する必要があることに注意してください。
終わりに
この投稿が、Swiftで利用可能なさまざまなタイプのエラー処理手法の違いを明らかにするのに役立つことを願っています。私のアドバイスは、1つのテクニックに固執するだけでなく、状況に応じて最も適切なテクニックを選択することです。一般に、エラーを致命的なものとして扱う必要がない限り、ユーザーエクスペリエンスを妨げないように、可能な限りエラーからの回復を常に試みることをお勧めします。
また、print(error)はエラー処理ではないことを忘れないでください😉
元の記事
Picking the right way of failing in Swift Swift by Sundell