はじめに
2020/03/29現在、Swiftではスローするエラーの型を指定できません。
そのため、スローするエラーの型が決まっていたとしても、すべてのエラーをキャッチする必要があります。
MyError というエラーの型のみを返す myFunc(isSuccess:) メソッドを例に考えてみます。
(この例では MyError.bar は返ることがないのですが、返る可能性があると考えてください)
enum MyError: Error {
case foo(_ message: String)
case bar(_ message: String)
}
func myFunc(isSuccess: Bool) throws -> String {
if isSuccess {
return "Success!!"
} else {
throw MyError.foo("Foo")
}
}
myFunc(isSuccess:) メソッドを呼び出します。
エラーの型を MyError だと決めていますが、一番下に } catch { を書いてすべてのエラーをキャッチする必要があります。
do {
let result = try myFunc(isSuccess: false)
// `result` を使う処理
} catch MyError.foo(let message) {
// エラーハンドリング
} catch MyError.bar(let message) {
// エラーハンドリング
} catch {
// !!!: 絶対に入らないけど必要
}
もし一番下の } catch { を書いていないと、以下のビルドエラーが発生します。
Errors thrown from here are not handled because the enclosing catch is not exhaustive
ここからスローされたエラーは、囲んでいるキャッチが完全ではないため処理されません
Switch文で default: をできる限り書かないようにするのと同様、絶対に入らないとわかっているエラーに対してハンドリングしたくありません。
絶対に入らないことを明確にするため、 fatalError(_:) を使っている人も多いと思います。
} catch {
fatalError("Unexpected error: \(error).")
}
いずれにせよスマートではないように感じます。
Twitterでスマートな方法を教えていただいたので紹介します。
環境
- OS:macOS Mojave 10.14.6
- Swift:5.1.3
- Xcode:11.3.1 (11C504)
結論
先に結論を述べます。
Result.init(catching:) と Result.mapError(_:) を使って実現します。
let result = Result { try myFunc(isSuccess: false) }
.mapError { $0 as! MyError }
switch result {
case .success(let string):
// `string` を使う処理
case .failure(let error):
switch error {
case .foo(let message):
// エラーハンドリング
case .bar(let message):
// エラーハンドリング
}
}
こうすることで、 MyError 型のみをハンドリングできるようになりました。
エラーを列挙型で定義している場合、Switch文が使えるので、エラーのハンドリング漏れも防げます。
解説
おそらくSwiftに慣れていても、上記のような書き方を知らない人は多いと思います。
少なくとも私は知りませんでした。
1つずつ解説します。
Result.init(catching:)
まずは1行目です。
let result = Result { try myFunc(isSuccess: false) }
こちらは Result 型のイニシャライザを、トレイリングクロージャを使って呼び出しています。
Result 型には Result.init(catching:) という、スローするクロージャを引数に持つイニシャライザがあります。
クロージャの処理が成功した場合は .success を返し、クロージャの戻り値がAssociated Valueに代入されます。
失敗した場合は .failure を返し、エラーがAssociated Valueに代入されます。
つまり、今回は myFunc(isSuccess:) が String を返すため、Result<String, Error> という型が生成されます。
Result.mapError(_:)
次に2行目です。
.mapError { $0 as! MyError }
こちらは失敗時のエラーの型をマップ(変換)するメソッドです。
Result.init(catching:) で生成した型は Result<String, Error> です。
mapError(_:) で Error 型を MyError 型に変換し、 Result<String, MyError> を生成します。
ここまでできたら、あとは通常の Result 型と同じように使えます。
switch result {
case .success(let string):
// `string` を使う処理
case .failure(let error):
switch error {
case .foo(let message):
// エラーハンドリング
case .bar(let message):
// エラーハンドリング
}
}
おまけ:なぜエラーの型を指定できないか考える
個人的な意見ですが、例えば throws の後ろにエラーの型を記述し、スローするエラーの型を指定できると使いやすいと思いました。
func myFunc(isSuccess: Bool) throws MyError -> String {
なぜこれが実装されないか考えたのですが、おそらく普通に実装しても様々なエラーが発生し得るからだと思いました。
自分では MyError 型しかスローしていないつもりでも、実装ミスで他のエラーがスローされる可能性は0ではありません。
実際、今回紹介した方法も as! MyError として強制キャストすることで実現しています。
おわりに
これでエラーをスマートにハンドリングできるようになったと思います!
GitHubにサンプルのplaygroundを上げたので、よかったら実行してみてください。
https://github.com/uhooi/SwiftErrorResultSample/blob/master/ErrorResult.playground/Pages/Sample.xcplaygroundpage/Contents.swift
他にもいい方法がありましたら、コメントなどで教えていただけると嬉しいです。