はじめに
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
他にもいい方法がありましたら、コメントなどで教えていただけると嬉しいです。